Escaping the relative import path hell, in Javascript
Why this subject?
First of all, I would like to justify this little article by explaining how I came to write on this particular subject.
Newly arrived at TheFork, and as an adept of every good practices and anything that can simplify our lives as developers regarding the code we write, I realized that the management of import paths was sometimes managed “at its simplest” by using relative paths, with all the problems of readability and refactoring complexity that this can cause in the long run.
Thus, I started the work on this article with the aim of summarizing the usefulness of both ways of doing things (absolute & relative), and showing how to take advantage of them, by keeping the best of both worlds!
Once upon a time…
…there was a large project, composed of many nested folders with a certain depth. The slightest modification of the file structure would affect a large number of other files, which used relative imports only.
As good and lazy developers, it is this consequence that we will try to minimize here, by comparing the advantages and disadvantages of imports via using relative and absolute paths.
Let’s take this small tree as an example for the rest of the explanations:
(Close) Relative imports
Relative import paths are based on the context where the importing file is located. Close relative imports (at the same tree level) are simple.
Here, we want to import a file close to the file from which we define the import, for example a style file for our component name “MyComponent”, positioned at the same tree level:
import * as S from './MyComponent.styles';
For React components, style files are strongly linked and are often put in the same folder, as for test files. Those files might always stay in the same folder.
(Distant) Relative imports
For imports made with relative paths done on remote files in the tree structure, it is necessary to navigate folder by folder to reconstruct our path, always by starting the path at the location of the file performing the import towards the targeted file.
For example, if we want to import “OtherComponent.tsx” from our component “MyComponent”:
import { OtherComponent } from '../OtherComponent';
The problem with long relative imports
As relative imports paths start their path from the location of the importing file, this means that when we start in the depths of a file structure and seek to import another one that exists in a very distant level of the tree structure, this will lengthen the path.
If we try to import a file containing utility functions “MyUtils.tsx”, located two levels above “MyComponent.tsx”:
import MyUtils from '../../utils/MyUtils';
This path writing is not very convenient and can quickly become difficult to read, the deeper it is.
Now, let’s imagine an import of a very distant component “AnyComponent”:
import { AnyComponent } from '../../../../../AnyComponent';
Here, the nightmare begins!
In large projects, relative imports therefore become a hell of a task to manage and understand… In the event of code refactoring, this will force us to rewrite all relative imports similar to this example.
Absolute imports
The other way to specify an import path is the absolute way. It allows to start from an arbitrarily defined starting point, where the relative path starts from the context of the current file instead.
For typescript projects, we must start by configuring our compiler with the “tsconfig.json” file, so that it can correctly interpret the absolute paths that we are going to define:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"components/*": ["components/*"]
}
}
}
Here, we indicate a “baseUrl” at the same level as the “tsconfig.json” (therefore at the root of the project), then “paths” with a name that will be used for our imports and a path that starts from the defined “baseUrl”. Let’s start by defining a path that will start from the “components” folder.
There you go, we now can use absolute imports!
Let’s try it by importing the “OtherComponent.tsx” from earlier:
import { OtherComponent } from 'components/OtherComponent';
We managed to import something without having to navigate through the tree levels, and this solution is much more readable and easy to use!
But, there is still a problem: we cannot easily distinguish module imports from absolute imports, and this can lead to confusion.
How to distinguish module imports
To do things right, we can try to imagine an easily recognizable syntax that will indicate that we are performing an import with an absolute path.
In the example below, we import two modules, React and Apollo:
import React from 'react';
import { useMutation, useQuery } from '@apollo/client';
We can define for example a beginning of syntax completely different from the import syntaxes of modules, let’s say “@/…” for example.
This syntax would allow us to immediately see that we are indicating an absolute path!
So let’s define this new syntax in our “tsconfig.json”, and while we’re at it, let’s also add the absolute path for the “utils” folder:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/components/*": ["components/*"],
"@/utils/*": ["utils/*"]
}
}
}
From now on, each time we want to import something contained in the “component” folder, we will write “@/components/…”!
import { OtherComponent } from '@/components/OtherComponent';
…and in the same way for the “utils” folder, we will write “@/utils/…”:
import MyUtils from '@/utils/MyUtils';
The good thing about absolute imports is that not matter the context they are used from, the path will stay the same! That will be helpful in case of a refactoring of the files that use those imports.
Make Jest work with absolute imports
One of the drawbacks of absolute paths is that you have to configure some packages accordingly, so that they can understand our path definitions.
In order to use Jest, a Javascript testing framework, we have configure it so that our tests continue to work, since we are now using absolute paths in our project, as it won’t understand what those paths all about without a bit of help:
import type { Config } from 'jest';const config: Config = {
moduleNameMapper: {
'@/components/(.*)': '<rootDir>/components/$1',
'@/utils/(.*)': '<rootDir>/utils/$1',
},
};export default config;
The code above is directly inspired from Jest’s documentation.
Pros & cons for relative and absolute paths
Relative paths
💚 Useful for imports at the same tree level
💚 No configuration needed
💔 The deeper you go in folders, the worse it gets
💔 Makes code refactoring painful
💔 Readability issues
Absolute paths
💚 No ../../../../ hell
💚 Easy to copy/paste
💚 Same path throughout the whole project
💚 Easy to read
💔 Need a bit a configuration
💔 …configuration for some packages too!
Hybrid solution (absolute & close relative imports)
By reading those pros & cons, can we find an even better way of doing imports?
An hybrid solution, by using:
- Relative paths for imports at the same level for files that have a strong link (such as styles for a component);
- Absolute paths for imports in general.
In the event of refactoring or moving code, this will greatly simplifies the changes to be made in the project. As a final example of all of these notions working together:
import React from 'react';
import { useMutation, useQuery } from '@apollo/client';
import { MyUtils } from '@/utils/MyUtils';
import * as S from './MyComponent.styles';
It looks like we finally have something that is both easy to understand and to use!
Is it finally over?
We have seen how to take the best of the two types of import paths, in order to have a more maintainable, more readable and understandable code. Any future refactoring should also be much less difficult, since we’ve made sure that wherever the imports are made from, it doesn’t change the relative or absolute import paths that we use, as long as our hybrid method is correctly used.
Many thanks to you for taking the time to read, I hope you enjoyed the reading!
One last thing I have to do now is asking to you the following question: for your projects, did you manage your imports in a different way, what were your issues with imports, and how did you solve them?