Lesson 21. Using modules

After reading lesson 21, you will

  • Understand how to specify the location of a module you intend to use
  • Understand all the various ways of importing values from modules
  • Understand how to import modules for side effects
  • Understand the order of execution of code when importing modules
  • Be able to break apart large modules into smaller ones

Modules are a great way to separate your logic into cohesive units and share logic across files without the cumbersome use of global variables. They also allow you to import only what is needed, keeping the cognitive load down and making maintenance easier. In the previous lesson you learned what a module is and the basics of how to create a module and export values. In this lesson we’ll look at using other modules, the various ways of importing values, and how to break apart and organize your code using modules.

Consider this

Imagine that you’re writing a web application and you need to use a few third-party open source libraries. The problem is that two of those libraries both expose themselves using the same global variable. How would you make the two libraries work together?

21.1. Specifying a module’s location

You import code from other module files using the import statement, which is made up of two key pieces, the what and the where. When you use the import statement, you must specify what (variables/values) you’re importing, and from where (the module file) you’re importing them. This is in contrast to including multiple files by using several <script> tags and communicating or sharing values across files by using global variables. The basic syntax is import X from Y where X specifies what you’re importing and Y specifies where the module is. A simple import statement may look like this:

import myFormatFunction from './my_format.js'

Then when you specify where or from what module you’re importing, you have to use a string literal value. The following is not valid:

const myModule = './my_format.js'
import myFormatFunction from myModule            1

  • 1 Invalid because myModule is a variable, not a string literal

You can’t use a variable to define where (from) the module is, even if that variable points to a string. You must use a string literal. This again is because all imports and exports in JavaScript are designed to be statically analyzable. All of your imports will be executed before any other code in the current file. JavaScript will scan your file, figure out all the imports, execute those files first, and then run your current file with the correct values imported. This means you couldn’t import based on a variable because that variable wouldn’t be defined yet!

Other than using a string to determine where a module is, there aren’t any other rules set forth by JavaScript. There will be what are called loaders for each JavaScript environment (mainly the browser and Node.js), and those loaders will define what the string actually looks like. Loaders for the web and Node.js are still being figured out. Today, most people using ES6 modules are doing so using something such as Browserify or Webpack. Both of these tools treat a file path such as ./file or ./file.js as relative to the current file:

import myVal from './src/file'           1
import myVal from './src/file.js'        1

  • 1 Both are equivalent, specifying the relative path to the file.

The file extension is optional and most people omit it. Without a file extension, the path could also be a directory containing an index.js.

import myVal from './src/file'                1
import myVal from './src/file.js'             2

  • 1 Matches both ./src/file.js and ./src/file/index.js
  • 2 Matches only ./src/file.js

Specifying a name without a path such as jquery would indicate that this is an installed module that should be looked for in the node_modules directory.

Now that you know how to specify where the module is, let’s take a look at how to specify what you want to import from it.

Quick check 21.1

Q1:

Where would the presumed loader most likely look for the module files for the following imports:

import A from './jquery'
import B from 'lodash'
import C from './my/file'
import D from 'my/file'

QC 21.1 answer

A1:

  1. From the file ./jquery.js or ./jquery/index.js relative to current file.
  2. From the main field specified in node_modules/lodash/package.json.
  3. From the file ./my/file.js or ./my/file/index.js relative to current file.
  4. From either ./file.js or ./file/index.js relative to the main field specified in node_modules/my/package.json.

 

21.2. Importing values from modules

In the previous lesson you created a module for formatting currency strings. It had a single default export of a function to achieve this like so:

export default function currency(num) {
  // ... return a formatted currency string
}

Let’s say you put this module in the location ./utils/format/currency.js and want to import the currency function to format some currencies in the shopping cart system you’re building. You could do so using the following import statement:

import formatCurrency from './utils/format/currency'           1

function price(num) {
  return formatCurrency(num)
}

  • 1 You’re importing the default value.

Notice how the name of the function was currency but you imported using the name formatCurrency. When you say import <name> you’re deciding what name to use, just like when you use var <name>, const <name>, or let <name>, only instead of assigning a value using the equals sign, you’re importing a value from another file, a module. You can use any name you want when importing the default value from a module, as you can see in the following silly example:

import makeNumberFormattedLikeMoney from './utils/format/currency'

function price(num) {
  return makeNumberFormattedLikeMoney(num)
}

The previous two examples would behave exactly the same, assuming there were no changes to the ./utils/format/currency module they were importing from. Remember the analogy from the previous lesson: If you were to think of a module as a function, the default export would be analogous to the return value from the function. If we continue to use this analogy, importing the default value would be like assigning the return value of a function to a variable:

function getValue() {
  const value = Math.random()
  return value
}

const value = getValue()
const whatchamacallit = getValue()

Notice how the getValue function returned a variable called value, and it didn’t matter if you assigned that to a matching named variable, or a variable with a completely different name such as whatchamacallit. This is because the function only returns a single value, and you’re merely capturing that value and deciding on a name to store it. This is the same when you import a default value from a module. The module can only export a single default value, and it only exports the value, so when you import it, you specify any name you want for storing that value.

Now as you learned in the previous lesson, in addition to a single default export, a module can also have one or more named exports. As the name implies, with these the name does matter. One of the syntaxes you learned in the previous lesson for named exports was the following:

function currency(num) {
  // ... return a formatted currency string
}

function number(num) {
  // ... return a number formatted with commas
}

export { currency, number }

Conveniently, the syntax to import these is similar:

import { currency, number } from './utils/format'

function details(product) {
  return `
    price: ${currency(product.price)}
    ${number(product.quantityAvailable)} in stock ready to ship.
  `
}

Here the names currency and number must match the names they were exported as. But you can specify a different name to assign them to:

import { number as formatter } from './utils/format'        1

  • 1 Specified to import number but assign it to the name formatter

This is handy if you’re importing named values from multiple modules that export using the same name. Say, for example, that you’re importing a function called fold from a functional tools module, but also importing a function called fold from an origami module. You could use as to map one or both of the imports to a different name to avoid naming conflicts:

import { fold } from './origami'
import { fold as reduce } from './functional_tools'            1

  • 1 fold is imported under the name reduce to avoid conflict with the other imported value.

If you want to import all of the named exports from a module, you can do so using the asterisk like so:

import * as format from './utils/format'         1

format.currency(1)
format.number(3000)

  • 1 A new object named format is created and assigned all the values from the module.

This will create a new object with properties correlating to all the named exports from the module. This is handy if you need to import all values a module exports, possibly for introspection or testing, but normally you should only import what you’re going to use. Even if you happen to be using everything that a module exports, it doesn’t necessarily mean that you’ll continue to use everything from that module as it grows.

Imagine, for example, that the format module only exported the format functions currency and date, and you need both for your product module, so you import them all using the asterisk. But later as you’re building your application, you continue to add new format functions to your format module. You don’t need these new functions in your product module, but since you were importing using the asterisk, you’re going to continue to get them all, not just the ones you’re using. Some situations may require you to import everything, but as a general rule, you should be explicit in what you import by specifying each value by name.

When importing all values using the asterisk, this doesn’t include the default export, just the named exports. You can combine importing the default and named exports from a module by separating them with a comma:

import App, * as parts from './app'                                     1
import autoFormat, { number as numberFormat } from './utils/format'     2

  • 1 The default value gets named App and all the named exports are set as properties of a newly created object called parts.
  • 2 The default value gets named autoFormat and the value exported by the name number gets named numberFormat.

Once you import a value from a module, it doesn’t create a binding like when declaring a variable. In the next section we’ll look at how that works.

Quick check 21.2

Q1:

In the following import statement, which is the default import and which is the named import?

import lodash, { toPairs } from './vendor/lodash'

QC 21.2 answer

A1:

lodash is the default import and toPairs is the named import.

 

21.3. How imported values are bound

Both default and named imports create read-only values, meaning you can’t reassign a value once it’s imported:

import ajax from './ajax'

ajax = 1                      1

  • 1 Error: ajax is read only

But named imports, unlike default imports, are bound directly to the variables that were exported. This means that if the variable changes in the file (module) that exported it, it will also change in the file that imported it.

Let’s imagine a module that exports a variable named title that has an initial value but also exports a function called setTitle that allows you to change the title like so:

export let title = 'Java'
export function setTitle(newTitle) {
  title = newTitle
}

If you were to import both, you couldn’t directly change the value of title via assignment, but you could indirectly change the value of title by calling setTitle:

import { title, setTitle } from './title_master'

console.log(title)         1
setTitle('Script')
console.log(title)         2

  • 1 “Java”
  • 2 “Script”

This is very different from how you’re used to retrieving values in JavaScript. Normally when you retrieve a value in JavaScript—either from a function call or destructuring or some other expression—and you assign it to a variable, you’re retrieving the value, and creating a new binding pointing to that value. But when you import values from modules, you’re importing not just the value but also the binding. This is why the module can internally change the value and your imported variable will reflect the change.

Once the value changes, it not only changes in the current file and the file that exported the value, but it also changes in all files that imported that value. On top of that, there’s no notification that the value changed. There’s no event broadcasting the change. It changes silently, so take care when changing values that you’ve exported.

In the next section, you’ll learn how and why you would import a module without importing any values at all.

Quick check 21.3

Q1:

There are five bindings in the following snippet. Which ones can be reassigned values in the shown context?

import a, { b } from './some/module'
const c = 1
var d = 1
let e = 1

QC 21.3 answer

A1:

Only d and e.

 

21.4. Importing side effects

Sometimes you just want to import a module for side effects, meaning you want the code in the module to execute, but you don’t need a reference to use any values from that module. An example of this would be a module that contains the code that sets up Google Analytics. You wouldn’t need any values from such a module; you just need to execute so it can set itself up.

You can import a module for side effects like so:

import './google_analytics'

It’s just like any other import; you omit any default or named values and you omit the keyword from as well. When you import a file for side effects, all of the code from the module you import will execute before any of the code in the file from which you imported it. This is regardless of where the import happens:

setup()

import './my_script'

In the previous example, all of the code in the module my_script is executed before the setup() function is executed, even though it’s imported afterward.

In the next section we’ll take a look at organizing and grouping smaller modules into bigger modules.

Quick check 21.4

Q1:

Assume the module log_b contains the statement console.log('B'). After running the following code, what will be the order of the output?

console.log('A')
import './log_b'
console.log('C')

QC 21.4 answer

A1:

B, A, C

 

21.5. Breaking apart and organizing modules

Sometimes a module will grow too big, and it may make sense to break it apart into smaller modules. But you may have a large code base that’s already using this module all over the place. You want to refactor this module into smaller, more focused modules, but you don’t want to have to refactor your entire application because of it. Let’s explore how you can take a module that’s already in use and break it down into smaller chunks without having any effect on the rest of your application.

Let’s assume you have a format module. It starts off small with just a few format functions, but as the application grows, you continue to need new formatters for different needs. Some formatters share logic, while others need their own helper functions. Keeping all of this in a single module has grown too complex. For brevity let’s assume four formatters, as shown in the following listing; in a real application, it could be many more.

Listing 21.1. src/format.js
function formatTime(date) {
  // format time string from date object
}

function formateDate(date) {
  // format date string from date object
}

function formatNumber(num) {
  // format number string from number
}

function formatCurrency(num) {
  // format currency string from number
}

Now let’s assume many modules are using these, such as the following product module.

Listing 21.2. src/product.js
import { formatCurrency, formatDate } from './format'

This product module is just one of many that are using formatters. You want to refactor your format module in a way that won’t break this one or any others.

Break the module into two separate modules, one for numbers and one for dates, and group them in a format folder.

Here’s the date format module.

Listing 21.3. src/format/date.js
function formatTime(date) {
  // format time string from date object
}

function formateDate(date) {
  // format date string from date object
}

And here’s the number format module.

Listing 21.4. src/format/number.js
function formatNumber(num) {
  // format number string from number
}

function formatCurrency(num) {
  // format currency string from number
}

You’ve nicely broken your large format module into smaller, more focused, modules. But to make use of these, you would have to refactor all of the other modules, like the product module, that are importing values. You want to prevent that. Create another index module inside the format module that imports the values from the more focused modules and exports them, as shown in the next listing.

Listing 21.5. src/format/index.js
import { formatDate, formatTime } from './date'
import { formateNumber, formatCurrency } from './number'

export { formatDate, formatTime, formatNumber, formatCurrency }

This is great because now when other modules try to import from ./src/format, it will actually import from ./src/format/index.js if ./src/format.js isn’t found. This means you no longer need to refactor any of the other modules. This is a great argument for omitting the file extension when specifying module paths in your imports, because if you had specified the file extension, this refactoring would have been much more painful.

This type of organization is so common that there’s actually a syntax directly for it. The src/format/index.js module could be written like so.

Listing 21.6. src/format/index.js
export { formatDate, formatTime } from './date'
export { formateNumber, formatCurrency } from './number'

If your only use for a value you’re importing is to turn around and export it, you can skip a step and export it directly from that module! Now, the format module is always supposed to export all the values from all the more focused formatters, right? Well instead of having to list all the names and then come back and add names for any new formatters you add in the future, you can export them all like so.

Listing 21.7. src/format/index.js
export * from './date'
export * from './number'

Cool! With this simple facade type module, you’ve successfully and quite elegantly broken your large module into much smaller and more focused modules, and you did it seamlessly in a way that’s opaque to the rest of the application!

Quick check 21.5

Q1:

Assume you are going to add another module at ./src/format/word and update the index file to export all of the word formatters.

QC 21.5 answer

A1:

export * from './date'
export * from './number'
export * from './word'

 

Summary

In this lesson, you learned how to use and organize modules.

  • Default imports can be set using any name.
  • Named imports are listed in braces, similar to how they’re exported.
  • Named imports must specify the correct name.
  • Named imports can use alternate names via as after specifying the correct name.
  • You can import all named values using an *.
  • Named imports are direct bindings (not just references) to the exported variables.
  • Default imports aren’t direct bindings but are still read-only.
  • Values can be exported directly from other modules.

Let’s see if you got this:

Q21.1

Make a module that imports the luck_numbery.js from the previous lesson and attempts to guess the lucky number and log how many attempts it made before guessing the correct number.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.162.26