Article summary
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.
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.
Thanks, the plugin will save my another day!
thanks for your sharing !
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.
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.
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.
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
Someone stole your post :/ https://habr.com/en/post/447506/