This article provides four best practices on how to better organize JavaScript modules.

Use named exports whenever possible

When you start using JavaScript modules, use Export Default to export the individual blocks defined by the module, whether classes or functions. Such as:

// greeter.js
export default class Greeter {
  constructor(name) {
    this.name = name;
  }

  greet() {
    return `Hello, The ${this.name}! `; }}import x from './greeter.js'
Copy the code

Over time, especially during large-scale refactoring, it can be difficult to find references to Greeter. Maintainability is greatly reduced. To make matters worse, the editor does not suggest that we write the imported class name ourselves.

So we turned to named exports. Let’s look at the benefits:

// greeter.js
export class Greeter {
  constructor(name) {
    this.name = name;
  }

  greet() {
    return `Hello, The ${this.name}! `; }}import {Greeter} from './greeter.js'
Copy the code

Using named Exports, the editor makes renaming even better: every time you change the original class name, all user modules automatically change the class name.

The editor also offers suggestions for avoiding blind typing, as follows:

Therefore, it is recommended to use named exports to benefit from renaming refactoring and code completion.

Note: Default import is usually available when using third-party modules like React, Lodash, etc. Because their import name is a constant: React, _.

Lazy import object

Module-level scopes should not do heavy computation, such as parsing JSON, making HTTP requests, reading local storage, and so on.

For example, the following module configuration resolves the configuration from the global variable bigJsonString

// configuration.js
export const configuration = {
  // Bad
  data: JSON.parse(bigJsonString)
};
Copy the code

This is a problem because bigJsonString parsing is done at the module level. When importing the configuration module, you will actually parse bigJsonString:

// Bad: parsing happens when the module is imported
import { configuration } from 'configuration';

export function AboutUs() {
  return <p>{configuration.data.siteName}</p>;
}
Copy the code

At a higher level, the role of a module-level scope is to define module components, import dependencies, and export common components: this is the dependency resolution process. It should be separate from the runtime: parsing JSON, making requests, and handling events.

Let’s refactor the configuration module to perform deferred resolution

// configuration.js
let parsedData = null;

export const configuration = {
  // Good
  get data() {
    if (parsedData === null) {
      parsedData = JSON.parse(bigJsonString);
    }
    returnparsedData; }};Copy the code

Because the data property is defined as a getter, bigJsonString is parsed only when the consumer accesses Configuration. data.

// Good: JSON parsing doesn't happen when the module is imported
import { configuration } from 'configuration';

export function AboutUs() {
  // JSON parsing happens now
  return <p>{configuration.data.companyDescription}</p>;
}
Copy the code

Consumers know better when to make big moves. The consumer may decide to do this when the browser is idle. Alternatively, a consumer might import a module but, for some reason, not use it. This provides an opportunity for further performance optimization: reducing interaction time and minimizing mainline work.

When importing, the module should not perform any heavy work. Instead, consumers should decide when to perform run-time operations.

Write highly cohesive modules

Cohesion describes how components within a module belong to the whole. Functions, classes, or variables of highly cohesive modules are closely related. They focus on one task. The formatDate module is highly cohesive because its functionality is closely related.

// formatDate.js
const MONTHS = [
  'January'.'February'.'March'.'April'.'May'.'June'.'July'.'August'.'September'.'October'.'November'.'December'
];

function ensureDateInstance(date) {
  if (typeof date === 'string') {
    return new Date(date);
  }
  return date;
}

export function formatDate(date) {
  date = ensureDateInstance(date);
  const monthName = MONTHS[date.getMonth())];
  return `${monthName} ${date.getDate()}.${date.getFullYear()}`;
}
Copy the code

FormatDate (), ensureDateInstance() and MONTHS are closely related to each other. Deleting MONTHS or ensureDateInstance() breaks formatDate(): a sign of high cohesion.

Problems with low cohesive modules

Low cohesive modules, on the other hand. Components that are unrelated to each other. The utils module below has three functions that perform different tasks.

// utils.js
import cookies from 'cookies';

export function getRandomInRange(start, end) {
  return start + Math.floor((end - start) * Math.random());
}

export function pluralize(itemName, count) {
  return count > 1 ? `${itemName}s` : itemName;
}

export function cookieExists(cookieName) {
  const cookiesObject = cookie.parse(document.cookie);
  return cookieName in cookiesObject;
}
Copy the code

GetRandomInRange (), Pluralize () and cookieExists() deleting a function does not affect the functionality of the entire module.

A low-cohesion module forces the consumer to rely on modules it doesn’t need, which creates unwanted transitive dependencies.

For example, the component ShoppingCartCount imports the Pluralize () function from the utils module

// ShoppingCartCount.jsx
import { pluralize } from 'utils';

export function ShoppingCartCount({ count }) {
  return (
    <div>
      Shopping cart has {count} {pluralize('product', count)}
    </div>
  );
}
Copy the code

Although the ShoppingCartCount module only uses the pluralize() function outside of the utils module, it has a transitive dependency on the cookie module (which is imported into utils).

A good solution is to split the low-cohesion module utils into several high-cohesion modules: utils/random, utils/stringFormat, and utils/cookies.

Now, if the ShoppingCard module imports utils/stringFormat, it has no pass-through dependency on cookies:

// ShoppingCartCount.jsx
import { pluralize } from 'utils/stringFormat';

export function ShoppingCartCount({ count }) {
  // ...
} 
Copy the code

The best examples of highly cohesive modules are Node built-in modules such as FS, PATH, assert.

It is recommended to write highly cohesive modules with tightly related functionality, classes, and variables. You can do this by refactoring large, low-cohesive modules into multiple high-cohesive modules.

Avoid long relative paths

In the following code, it’s hard to know the parent component, and you’ll have to rewrite the path again and again if the file changes.

import { compareDates } from '.. /.. /.. /date/compare';
import { formatDate }   from '.. /.. /.. /date/format';

// Use compareDates and formatDate
Copy the code

Therefore, I recommend avoiding relative paths and using absolute paths instead:

import { compareDates } from 'utils/date/compare';
import { formatDate }   from 'utils/date/format';
Copy the code

Although absolute paths can sometimes be long to write, they are used to clearly show the location of the imported module. We can easily define absolute paths using babel-plugin-module-resolver.

Use absolute paths instead of long relative paths.

The address

The last