Relative Requires in JavaScript
In a JavaScript codebase of sufficient size, you’ll likely run into the need to import
or require
files from a different part of the codebase. Too much of this can be a sign of a system that isn’t structured very well, but even in a well-architected, modular codebase, there will be dependencies between the modules.
So what do you do when you get tired of typing import MyComponent from '../../../modules/MyModule'
?
Part of it depends on the environment and toolset you’re using. Different tools have different options for addressing this problem. You also have to think about how other tools will deal with the solution, such as editors/IDEs, linters, etc.
The answers might be different for libraries than they are for applications. In this post, I’m focused mainly on applications.
There’s a very good summary and subsequent discussion about this issue for Node.js in a Gist by Bran van der Meer.
Here are few options I’ve considered or used.
NODE_PATH
There are a couple of ways of solving the problem with NODE_PATH
, and I’ve used that option the past. However, the Node.js module documentation says:
NODE_PATH is still supported, but is less necessary now that the Node.js ecosystem has settled on a convention for locating dependent modules. Sometimes deployments that rely on NODE_PATH show surprising behavior when people are unaware that NODE_PATH must be set. Sometimes a module’s dependencies change, causing a different version (or even a different module) to be loaded as the NODE_PATH is searched.
The Browserify Handbook also talks about this option:
You might see some places talk about using the $NODE_PATH environment variable or opts.paths to add directories for node and browserify to look in to find modules.
Unlike most other platforms, using a shell-style array of path directories with $NODE_PATH is not as favorable in node compared to making effective use of the node_modules directory.
This is because your application is more tightly coupled to a runtime environment configuration so there are more moving parts and your application will only work when your environment is setup correctly.
node and browserify both support but discourage the use of $NODE_PATH.
For those reasons, I’m not such a fan of this option any more.
Put Code in node_modules
The Browserify Handbook has some alternative suggestions, such as putting some of your modules under node_modules
, either directly or via symbolic links.
This seems like more hassle than it’s worth. You have to do interesting things with your .gitignore
to make it work, and also remember that node_modules
doesn’t just contain third-party dependencies.
Break Things Into Modules
If your application already has a modular structure, you could break it into separate npm packages. These could be private packages that you host in a private repository.
Nick Sellen does a great job talking about the various options for this approach.
When an application is under heavy development, it might not be clear where the module boundaries should be yet, so it would be premature to break it up. I prefer a solution that makes it easy to change my mind about where things belong and perform the subsequent refactoring.
Webpack resolve.root
If you’re using Webpack, you can use the resolve.root
setting, as described in an article by Grgur Grisogono.
In your webpack configuration file, add:
Using this setting, you can then do import MyComponent from 'modules/MyModule'
and webpack will find the file relative to your top-level client
directory.
If you also use ESLint and eslint-plugin-import, you can use its eslint-import-resolver-webpack so that ESLint can also find your modules.
In our projects at work, this is our preferred solution and we have it configured in our react-boilerplate project.
React Native
My reason for researching this topic is that I ran into the same import
problem in a new React Native project I’m working on. React Native doesn’t use Webpack, so we can’t use its resolve.root
setting like we normally do.
React Native allows for a new trick that doesn’t seem to work in the Node.js environment. It turns out that you can place a very minimal package.json
file at the root of your source tree:
With that in place, you can do import MyComponent from '@/modules/MyModule'
, where @
is the name used in the package.json
file.
The React Native package resolver will walk up the directory tree, find your package.json
file with the name @
, and resolve the import relative to the main
entry in that file.
This solution is described more fully by Mike Grabowski.
I ran into a use of this technique in the React Native Katas project.
While this is an interesting technique, it feels like it’s based on an internal implementation detail that may not work in the future.
Babel Plugin
In my research, I also ran into the suggestion to use a Babel plugin for this. If you’re already using Babel, this is a viable option. And if you’re using React Native, you are using Babel.
I saw a number of references to babel-plugin-module-alias
. That plugin has since been renamed to babel-plugin-module-resolver.
To use the plugin, you need to add it to your Babel configuration file (either .babelrc
or nested inside your package.json
file).
Note that React Native has its own Babel configuration. When you add your own Babel configuration to use this plugin, React Native will use it instead of its own configuration, so you have to make sure your configuration has what React Native needs. Fortunately, there is babel-preset-react-native that takes care of the details.
Here is the .babelrc
file I’m using on my project:
While I haven’t tried this plugin in a non-React Native project, there’s no reason it shouldn’t work elsewhere as well.
For additional tool support, there’s a resolver, eslint-import-resolver-babel-module that works with eslint-plugin-import.
The project README also provides advice about editor integration for Atom and IntelliJ/WebStorm.
Summary
I’ve outlined a number of ways I’ve found to solve the project-relative import
problem in JavaScript. I’ve provided a number of links to resources with much more detail than I’ve been able to provide here.
As I mention above, my current approach is to use Webpack’s resolve.root
setting for any project using Webpack, and to use babel-plugin-module-resolver
for React Native projects.