Skip to content

Add Aliased File Imports to Your Gatsby Project

8 min read

A tree with tangled branches.
Image credit: Faye Cornish

Have you ever written code like this?

import { CustomButton } from '../../../components/buttons/custom-button';
import { useCustomHook } from '../../../hooks/use-custom-hook';
js

As your project's codebase expands, it's easy to end up with multiple embedded layers of folders, and that can turn your file imports into a tangled web of dots and slashes.

This tutorial will walk you through converting your Gatsby project's relative imports into absolute, aliased imports:

import { CustomButton } from '@/components';
import { useCustomHook } from '@/hooks';
js

Configuring absolute imports

The first step to simplifying our file imports will be configuring our Gatsby project to support importing files relative to the project root, like:

import { TestComponent } from 'src/components/test-component';
js

There's a couple things we'll need to do to achieve this:

  1. Configure webpack to understand absolute imports.
  2. Configure other technologies that need to know about how to correctly import files (e.g., TypeScript or Jest).

Configuring webpack to support root imports

Because Gatsby uses webpack under the hood to bundle our code, we'll need to configure webpack to understand absolute imports. Webpack configuration can get fairly complex, but fortunately there's a Gatsby plugin that'll take care of the details for us: gatsby-plugin-root-import. Let's install it:

npm install gatsby-plugin-root-import
bash

Next, you'll need to add the plugin to your gatsby-config:

const config = {
// ...
plugins: [
// ... other plugins
`gatsby-plugin-root-import`,
],
};
export default config;
js

This plugin supports root imports out of the box, without any need for additional configuration. So the following should work automatically:

import { CustomButton } from 'src/components/buttons/custom-button';
js

Already that's an improvement over our old relative imports. It's easier to understand exactly where the file is, and the import statement won't break if the file we're importing to is moved.

Note: if you'd like to try configuring webpack yourself, you can do that through gatsby-node's onCreateWebpackConfig api. For more information, check out this how-to guide on configuring webpack from the Gatsby docs.

Adding TypeScript support for root imports

If your project uses TypeScript, you'll need to add another step to make the TypeScript compiler understand absolute imports.

In your tsconfig.json file, add the following:

{
"compilerOptions": {
// ...
"baseUrl": "."
}
}
json

This lets TypeScript know that whatever you set as the baseUrl (in this case, the project root) should be treated as the base directory when resolving non-relative file imports. So when TypeScript encounters src/components, it'll understand that this means ./src/components.

Upgrading to import aliases

Your project should now support absolute file paths in import statements. Great! But there are a few more tweaks we can do to make file imports even simpler.

First, we can use import aliases:

// without import aliases:
import { CustomButton } from 'src/components/buttons/custom-button';
// with import aliases:
import { CustomButton } from '@/components/buttons/custom-button';
js

Although that doesn't look like a big difference now, it's a starting point to make our imports much shorter later.

Configuring webpack to support aliases

To get this working, we'll again need to update our webpack config. Fortunately, gatsby-plugin-root-import can do the heavy lifting for us again. Update your gatsby-config like so:

import path from 'path';
const config = {
// ...
plugins: [
// ...
{
resolve: 'gatsby-plugin-root-import',
options: {
'@': path.join(__dirname, 'src'),
},
},
],
};
export default config;
js

So what's happening here? Every entry added to options will be treated by the plugin as a webpack import alias. So in other words, we've just told webpack to convert @ to src.

You can add as many import aliases as you like. For example, you could add a separate alias for @components or @hooks.

This is especially useful if you're using deeply nested folders. For example, the Redux docs recommend organizing your logic into "feature folders", like ./src/features/todos. If you choose to structure your program like that, you may find it convenient to alias ./src/features/todos as simply @todos.

Configuring TypeScript to support aliases

Just like before, we'll need to update TypeScript to mirror our new config. In your tsconfig.json file, add:

{
"compilerOptions": {
// ...
"paths": {
"@/*": ["src/*"]
}
}
}
json

Any entries added to paths will be re-mapped by TypeScript. Note that all mappings are relative to whatever was specified in the baseUrl. If you're interested, you can read more about tsconfig paths in the TypeScript docs.

Note: If you're using VS Code, you may need to restart it before the new import aliases are recognized.

Configuring Jest

If you're using Jest to test your Gatsby project, you'll need to update your Jest config to support import aliases. If you haven't already, make sure you've correctly configured Jest to work with Gatsby. If you need more information on that, the Gatsby docs have a great article on adding unit tests to Gatsby.

You can configure Jest to understand path aliases by adding them to moduleNameMapper in your jest.config.js. If your project uses TypeScript, you can easily import the path aliases from tsconfig.json using a utility function from ts-jest:

const { compilerOptions } = require('./tsconfig.json');
const { pathsToModuleNameMapper } = require('ts-jest');
const paths = pathsToModuleNameMapper(compilerOptions.paths, {
prefix: '<rootDir>/',
});
module.exports = {
// ...
moduleNameMapper: {
// ... other module names
...paths,
},
// ...
};
js

By dynamically importing your import aliases through pathsToModuleNameMapper, Jest will always be in sync with TypeScript, and any time you update your import aliases in tsconfig.json, the changes will be automatically recognized by Jest. Otherwise, you'd need to manually specify each of the path aliases in jest.config.js:

module.exports = {
// ...
moduleNameMapper: {
// ... other module names
'^@/(.*)': '<rootDir>/src/$1', // configuring the @ alias
},
// ...
};
js

Re-export components

A final, optional step that I sometimes like to do in my projects is to re-export components from the base ./components directory.

For example, consider a project structured like this:

src
└── components
├── test-component-one.tsx
├── test-component-two.tsx
└── buttons
└── custom-button.tsx
markdown

In the buttons folder, we can add an index.js or index.ts file, and re-export all the other files in buttons:

export * from './custom-button';
js

Then we can import any of the button components as:

import { CustomButton } from '@/components/buttons';
js

But we can take it one step further, and again re-export components from the root of ./components. In components/index.js:

export * from './test-component-one';
export * from './test-component-two';
export * from './buttons';
js

Now, any of our components can be imported directly from ./components as:

import { CustomButton } from '@/components';
js

This can also give you a higher degree of control over the encapsulation of different parts of your codebase. For example, say you have a todo list application, with a folder structure like this:

src
└── features
├── todos
├── index.ts
├── todos-slice.ts
├── utils.ts
├── TodoList.ts
└── Todo.tsx
├── user-settings
└── authentication
markdown

Imagine that utils.ts is part of the internal implementation of todos. As such, it isn't meant to be used by outside code, and doing so could cause some pretty big problems. If some other code outside of todos depended on utils, any refactor of the implementation of todos could unexpectedly cause the rest of your app to break! Obviously that wouldn't be ideal.

So to prevent problems like this, you might want to avoid exposing the functions in utils to the rest of your application. Now, you could just put in a comment telling your future self (and other developers working on the project) not to import it, and hope that works, but that's not a very reliable approach.

Instead, you could write your feature's index.ts file like this:

export * from 'todos-slice';
export * from 'TodoList';
export * from 'Todo';
ts

Notice that utils.ts isn't re-exported by index.ts. As long as you remember to always import a feature's code from the feature's root index.ts file, you can create a standard interface to control which parts of todos are exposed to the rest of your project, and which parts are hidden as implementation details.

Summary

And that sums it up! That's how you can transform your relative imports into absolute, aliased imports in your Gatsby project.