loading...
Cover image for [Updated] Simplify the require/import paths in your project and avoid ../../../  circles of hell

[Updated] Simplify the require/import paths in your project and avoid ../../../ circles of hell

mjraadi profile image Mohammadjavad Raadi Updated on ・4 min read

Do you hate seeing ../../../ everywhere in your code? Come along and I'll show you why you should use babel-plugin-module-resolver to work faster and write cleaner code.

Update 1 (3/31/19):

As Pavel Lokhmakov suggested, I've created a new GitHub repo here to achieve the functionality explained in this post without the need to eject the app.
react-app-rewired and customize-cra are both libraries which let you tweak the create-react-app webpack/babel config(s) without using 'eject'.
Simply install these packages as dev dependencies and create a new file called config-overrides.js in the project's root directory and put your custom config there. Then all you have to do is to update your npm scrips according to react-app-rewired docs.

The Inspiration

I never liked writing code like this:

import NavBar from '../../components/NavBar';

To me it seemed very confusing, not clean and not maintainable. Imagine somewhere down the line, you needed to alter your project's directory structure. You would have to go through every file and update your code to reflect your changes. Talk about non-maintainabilty!
But I loved the way I would import packages from the node_modules directory:

// ES6 import syntax
import React, { Fragment } from 'react';

// CommonJS require syntax
const nodemailer = require('nodemailer');

So I was eager to find a way to import/require my custom modules/components just like this. babel-plugin-module-resolver to the rescue!

TL;DR

You can find the GitHub repos associated with this article:
https://github.com/mjraadi/babel-plugin-module-resolver-test-app
https://github.com/mjraadi/babel-plugin-module-resolver-customize-cra

What does it do?

I'll let the plugin author explain:

This plugin can simplify the require/import paths in your project. For example, instead of using complex relative paths like ../../../../utils/my-utils, you can write utils/my-utils. It will allow you to work faster since you won't need to calculate how many levels of directory you have to go up before accessing the file.

In case you don't know what babel is, it's a JavaScript compiler which is mainly used to convert ECMAScript 2015+ code into a backwards compatible version of JavaScript in current and older browsers or environments. If you're building an app with create-react-app or similar libraries, they're using babel behind the scene.

Let's get started

Here I will show you how you can use this plugin in an app created by create-react-app. Create a new app with the command below:

$ create-react-app babel-plugin-module-resolver-test-app

create-react-app encapsulates the project setup and all the configurations and gives you tools to create production ready apps. Since we need to change babel configuration we need to eject our app. Ejecting will move create-react-app’s configuration files and dev/build/test scripts into you app directory.

Note: this is a one-way operation. Once you eject, you can’t go back!
It's fine for our use case because we're building a test app. Go ahead and run the command below:

$ npm run eject

Confirm and continue.

Note: at the time of writing this post, there is a bug with create-react-app explained here. The workaround is to remove the node_modules directory and reinstall the dependencies again.

Install the dependencies:

$ npm install

Install babel-plugin-module-resolver plugin by executing the following command in your project directory:

$ npm install --save-dev babel-plugin-module-resolver

Open package.json file and look for babel config. This is how it looks like after eject:

...
"babel": {
  "presets": [
      "react-app"
    ]
  },
...

Now we need to tell babel to use our module resolver and define our root directory and aliases. Edit your babel config section to make it look like this:

...
"babel": {
    "presets": [
      "react-app"
    ],
    "plugins": [
      ["module-resolver", {
        "root": ["./src"],
        "alias": {
          "dir1": "./src/Dir1",
          "dir2": "./src/Dir2"
        }
      }]
    ]
  },
...

Now create two directories in src directory called Dir1 and Dir2. Our defined aliases will point to these directories respectively.

Create a component file called ComponentA.js in Dir1 directory and put the code below in it:

import React from 'react';
import ComponentB from 'dir2/ComponentB';

const ComponentA = () => (
  <p>
    Hello from <ComponentB />
  </p>
);

export default ComponentA;

Now create ComponentB.js in Dir2 directory with the code below:

import React from 'react';

const ComponentB = () => (
  <a 
    href="https://www.bitsnbytes.ir"
    className="App-link"
    target="_blank"
    rel="noopener noreferrer"
  >
    Bits n Bytes Dev Team
  </a>
);

export default ComponentB;

Now edit the App.js file in the src direcory:

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import ComponentA from 'dir1/ComponentA';

class App extends Component {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <ComponentA />
        </header>
      </div>
    );
  }
}

export default App;

Notice that I didn't have to go up one directory or down another directory to import my components. We're now ready to run our app, run the command bellow in your terminal:

$ npm start

You should see your app in the browser without any problem. If you have any questions or problems, feel free to comment.

Conclusion

Custom module resolvers will save you time and the frustration of dealing with ../ splattered everywhere. They take a bit to setup and ensure full cooperation with existing tooling, but the result and visual satisfaction of never having to see ../../../../../.. is well worth the initial outlay on big projects.

About Me

I am a full stack web developer and co-founder of Bits n Bytes Dev Team, a small group of highly talented and professional freelance developers, where we provide custom web application development services based on cutting-edge technologies, tailored to client's unique business needs.

I'm available for hire and you can check out my portfolio website at https://www.bitsnbytes.ir/portfolio or contact me at raadi@bitsnbytes.ir.

Posted on by:

mjraadi profile

Mohammadjavad Raadi

@mjraadi

Full Stack JavaScript Developer | Building Web Apps | Working With Vue, React, Node.js & MongoDB | Open to Remote Roles

Discussion

markdown guide
 
  1. For CRA enough to create in root .env with NODE_PATH=src and all directories inside ./src will be available by absolute path.

  2. Do not use eject, we have alternatives to modify most of internal configs. U can choose:

  • customize-cra
  • react-app-rewired
 

Thanks Pavel, I'll have a look at them.

 

I've updated the post to reflect your suggestion. Thanks

 

For typescript, you need this in tsconfig.json:

"paths": {
      "src/*": ["src/*"],
      "components/*": ["src/components/*"],
      "hooks/*": ["src/hooks/*"],
      "hocs/*": ["src/hocs/*"],
      "fragments/*": ["src/fragments/*"],
      "utils/*": ["src/utils/*"],
      "markdown/*": ["src/markdown/*"]
    }
 

Great writeup! There is also webpack alias settings, which you can use to achieve a similar thing. That is what we are using, although since our jest tests don't run webpack, we have to add the alias to jest as well. (we've made our root project directory '@'). I wonder if baking it into babelrc and using same babelrc files would cover the setup in one place.

Incidentally with the webpack.alias approach I haven't got autocomplete in VS Code working despite having followed the instructions to get it to work.. (though maybe not close enough)

 

We got it to work adding aliasFields... might wanna give it a shot.

 
 

Having done this on past projects in the early days of Webpack, I don't recommend doing it today. It's like sweeping dust under your rug. Yah, it's out of sight but its still in your house.

What exactly is wrong ../../../../../..? It stores state information about your path, but more importantly it lets you know that you're stuff is nested deeply. In my mind, that deep nesting is itself a potential problem - maybe a code smell?

w.r.t to renaming, moving, or otherwise managing files, the work of updating and syncing paths should be handled by your IDE instead. Yes, it will cause new commits to be made for files that depend on those now moved files. But why is that bad?

The only case I can see is working on a team - but if you're files are constantly moving directories I think you have other problems. Plus, you'd still have to update the "alias": { "dir": "./srcDir1" } property in your .babelrc file anyways.

tldr - pointless optimization that adds dependencies, occludes path information, prettifies something that arguably doesn't need prettying up.

 

That's great. Vuejs has the same problem. I'm in trouble with that. But now I think it can be resolved by babel-plugin-module-resolver.
Thank you so much

 

If you're using the Vue webpack config, such as with SFC, it provides an alias you can use to reference files by absolute path (e.g. from '@/components/Button'). I've carried this convention into my React projects as it's really easy to setup without adding another dependency. Only downside is that it only works for webpacked files.

 

Thank you so much. I'm gonna try it

 

Yeah, I'm sure it's pretty much the same config because vue uses babel to compile as well. If you have a .babelrc file in your project directory, you can put the config in there. Take a look at the github page of the plugin for more info.

 

This can be a very useful plugin. I'm currently dabbling with mobile development, specifically React Native. Can I use it for that? I use VSCode as my IDE. Thanks!

 

Yes, I was able to get it to work with react-native as well. You need to configure VSCode to work with this plugin and make the auto complete functionality work.
Here's what you gotta do from the plugin's doc:
github.com/tleunen/babel-plugin-mo...

 
 

Thanks! Really good and straightforward explanation.