Article summary
Circular dependencies in JavaScript (also known as cyclic dependencies) occur when two or more modules reference each other.
This could be a direct reference (A -> B -> A
):
or indirect (A -> B -> C -> A
):
While circular dependencies may not directly result in bugs, they will almost always have unintended consequences. This could manifest as slow TypeScript type-checking or frequent dev-server “JavaScript heap out of memory” crashes, but could very well introduce run-time bugs. 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 Circular Dependencies
Fixing circular dependencies on a large project can be a significant time investment. That’s why it’s best to start with the circular dependency checker from the get-go, since you can prevent them from being introduced at all.
However, in software teams, we don’t always have the luxury (misfortune) of setting up the build configuration for a project, and often we inherit codebases. So it’s often necessary to address this after it becomes a problem.
Unfortunately, adding the circular dependency plugin and immediately seeing 500 errors feels overwhelming. But fear not! It’s not always one-to-one – sometimes fixing a single import may break the chain of imports for a large number of problems, especially when it’s in core logic or shared helpers. So those 500 errors may be solved by moving code around in just five files. (Probably not, but you can dream 😤)
In Practice
There’s not always a clean one-size-fits-all solution to address circular dependency imports. It just comes down to rolling up your proverbial sleeves and getting started with it.
Ultimately, this comes down to breaking the import chain. 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.
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. This needs to be determined on a case-by-case basis depending on unique factors for each import, but the general behavior should stand.
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.
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/