Eliminate Circular Dependencies from Your JavaScript Project

Circular dependencies (also known as cyclic dependencies) occur when two or more modules reference each other.

This could be a direct reference (A -> B -> A):

 
// file a.ts
import { b } from 'b';
...
export a;

// file b.ts
import { a } from 'a';
...
export b;

or indirect (A -> B -> C -> A):

 
// file a.ts
import { b } from 'b';
...
export a;

// file b.ts
import { c } from 'c';
...
export b;

// file c.ts
import { a } from 'a';
...
export c;

While circular dependencies may not directly result in bugs (they certainly can), they will almost always have unintended consequences. In our project, we were experiencing slow TypeScript type-checking and frequent dev-server “JavaScript heap out of memory” crashes.

Node.js does support circular require/import statements between modules, but it can get messy quickly. In the Node.js docs, it says, “Careful planning is required to allow cyclic module dependencies to work correctly within an application.”

In my experience, the best way to deal with circular dependencies is to avoid them altogether. Circular dependencies are usually an indication of bad code design, and they should be refactored and removed if at all possible.

Checking for Circular Dependencies

While there are quite a few Node packages that perform static analysis to look for circular dependencies, they didn’t quite do the trick. Some of the packages found a few circular dependencies, while others missed all of them completely. The best circular dependency checker that I found works at the bundling layer. This webpack circular-dependency-plugin was quite comprehensive and very simple to use.

To get started, I just copied the sample code from the circular-dependency-plugin docs:

 
// webpack.config.js
const CircularDependencyPlugin = require('circular-dependency-plugin')

module.exports = {
  entry: "./src/index",
  plugins: [
    new CircularDependencyPlugin({
      // exclude detection of files based on a RegExp
      exclude: /a\.js|node_modules/,
      // add errors to webpack instead of warnings
      failOnError: true,
      // allow import cycles that include an asyncronous import,
      // e.g. via import(/* webpackMode: "weak" */ './file.js')
      allowAsyncCycles: false,
      // set the current working directory for displaying module paths
      cwd: process.cwd(),
    })
  ]
}

Immediately, the plugin found all sorts of circular dependencies that had been introduced over the duration of the project.

Output of the circular dependency plugin
Output of the circular dependency plugin

Fixing the Circular Dependencies

There are a couple of options to get rid of circular dependencies. For a longer chain, A -> B -> C -> D -> A, if one of the references is removed (for instance, the D -> A reference), the cyclic reference pattern is broken, as well.

For simpler patterns, such as A -> B -> A, refactoring may be necessary. Perhaps the modules that live in B could be moved to A. Or, necessary code could be extracted to a C that both A and B reference. If the two modules perform similar behaviors, they could also be combined into a single module.

Fixing a large number of circular dependencies might be a significant time commitment, but it improves the maintainability of the codebase and can reduce bugs in the future. By leaving the circular dependency plugin in the webpack pipeline, it can be run frequently, and circular dependencies will be found immediately after introducing one.

The next time I’m starting a project and configuring webpack options, I’ll include the circular-dependency-checker from day one.

Conversation
  • chase says:

    Thanks, the plugin will save my another day!

  • Lancelot says:

    thanks for your sharing !

  • JACK says:

    How do I use the plugin? Should I add the code in my index.html file? Maybe I’m a noob but there’s less infos about the way to use the plugin.

    • Dan Kelch Dan Kelch says:

      Hi Jack,

      This plugin is designed for Webpack users – if you have a webpack.config.js file, you could follow the instructions laid out in the documentation:

      // webpack.config.js
      const CircularDependencyPlugin = require('circular-dependency-plugin')
      module.exports = {
      entry: "./src/index",
      plugins: [
      new CircularDependencyPlugin({
      // exclude detection of files based on a RegExp
      exclude: /a\.js|node_modules/,
      // include specific files based on a RegExp
      include: /dir/,
      // add errors to webpack instead of warnings
      failOnError: true,
      // allow import cycles that include an asyncronous import,
      // e.g. via import(/* webpackMode: "weak" */ './file.js')
      allowAsyncCycles: false,
      // set the current working directory for displaying module paths
      cwd: process.cwd(),
      })
      ]
      }

      Some tools (such as Next.js or create-react-app) will manage the Webpack configuration behind the scenes, but they typically allow for the user to override or customize the configuration themselves.

      If you aren’t using Webpack, then this particular plugin won’t help much, but there is likely another one to suit your needs.

  • Fatai Balogun says:

    Nice one, for those that aren’t using webpack in their project. Setting up *Eslint* is a good alternative to statically detect when circular dependencies is introduced to the codebase. eslint import/no-cycle should be enabled which is from airbnb style.

  • Robbert says:

    Instead of fixing it, you can avoid these problems and use init() functions. You can take control over the order in which the code is executed. See more at : https://stackoverflow.com/questions/38841469/how-to-fix-this-es6-module-circular-dependency/42704874#42704874

  • Alalanais says:

    Someone stole your post :/ https://habr.com/en/post/447506/

  • Comments are closed.