reactjstypescriptwebpackbabeljs

Absolute Imports: React and Typescript


Background

I have a React app bootstrapped using create-react-app and typescript. As the application has grown, (goal) I would like to implement absolute imports. I am using VS Code (Visual Studio Code) and with very little configuration, I got TS and VS Code to recognize my absolute imports.

For TS, I took the following steps in my tsconfig.json:

For VS Code, I changed my User Settings: Typescript -> Preferences: Import Module Specifier -> non-relative

That worked great. All of my imports were using absolute imports, no errors. But, when I ran the app, I got an error: Error: Cannot find module "component" I expected to see my app like I did before the absolute imports.

What I Tried

Figured, the error was a webpack or babel issue.

  1. Created env File

    Added the following to an env file in the root of the app (same location as my package.json)

NODE_PATH=client/

That did not work. Same error: Error: Cannot find module "components". Also tried changing NODE_PATH to REACT_APP_NODE_PATH that did not work either.

  1. Modify Babel Config

    Added babel plugin module resolver with yarn add -D babel-plugin-module-resolver. Then modified my babel.config.js to:

module.exports = { 
  env {...}, 
  plugins: [
    [
      'module-resolver',
      {
        cwd: 'babelrc',
        extensions: ['.ts', '.tsx', '.js'],
        alias: {
          client: './client',
        },
      },
    ],
  ]
}

That returns the same error. (I am restarting the server after every change to my config files)

Resources Referenced I used a lot of different articles to try to find clarity. Here are some:

And many others. None of that worked.

Project Structure

My project structure is a little "unconventional" or not my typical pattern which could be causing an issue.

└── root dir
    ├── assets
    │   └── client
    │       ├── assets
    │       ├── components
    │       ├── hooks
    │       └── ...
    │   └── babel.config.js
    │   └── .babelrc
    │   └── webpack.config.js
    │   └── package.json
    └── server files (no server dir) 

So client is like my src in a typical react app. assets is the "entry dir" for my server which is in the root dir.

Any help would be appreciated.


Solution

  • Found the answer. There is no need for the env file. Only need to modify the tsconfig.json and the webpack.config.js files.

    TS Config

    Added two keys: baseUrl and paths. Set the baseUrl to your src or client directory. The paths key will contain an object with whatever you want to reference in your absolute path.

    {
      "compilerOptions": {
        ...
        "baseUrl": "./client",
        "paths": {
          "client/*": ["./client/*"]
        },
        ...
      },
      "include": ["./**/*.ts", "./**/*.tsx"]
    }
    

    Webpack Config

    Added an alias for "src" (in my case, "client").

    module.exports = (env, options) => ({
      ...
      resolve: {
        alias: {
          client: path.resolve(__dirname, 'client/') // added this
        },
        extensions: ['.ts', '.tsx', '.js', '.jsx', '.json']
      }
    })
    

    Usage To import something now, I can replace the relative import with "client". This:

    import TableHeader from '../../../components/Table/TableHeader'
    

    becomes this:

    import TableHeader from 'client/components/Table/TableHeader'
    

    If there is a better way to do this, post your solution. 😀

    Update: 🥧 Day 2022

    To do this with newer versions of tsc or create-react-app

    npx tsc --init
    

    or

    npx create-react-app my-new-app --template typescript
    

    you only need to modify the baseUrl property in your tsconfig.json. If you want the base directory to be your root directory you would set baseUrl to . or src if you want it to be your source directory.

    Example

    File tree

    .
    └── my-new-app/
        ├── node_modules
        ├── src/
        │   ├── hello/
        │   │   └── world/
        │   │       └── foo/
        │   │           ├── bar.tsx
        │   │           └── sibling.tsx
        │   ├── components
        │   ├── layouts
        │   └── utils
        ├── index.ts
        ├── tsconfig.json
        ├── .prettierrc.json
        ├── package.json
        ├── yarn.lock
        └── .gitignore
    
    // baseUrl: "." root
    import Bar from "src/hello/world/foo/bar";
    
    // baseUrl: "src" dir
    import Bar from "hello/world/foo/bar";
    

    You can still specify paths if you would like however I find the baseUrl to be enough.

    VS Code

    If you are using VS Code and you do this and it does not work you may need to tinker with your typescript.preferences.importModuleSpecifier setting.

    Updating "typescript.preferences.importModuleSpecifier" Setting

    1. Go to your VS Code settings (not the JSON file the GUI)
    2. Search for "typescript.preferences.importModuleSpecifier"
    3. Verify that you have "shortest" or "non-relative" selected

    Shortest means it will use a "non-relative" import only if that is shorter than using a relative import.

    Example: Shortest vs. Non Relative Imports

    import Bar from "src/hello/world/foo/bar";
    import Bar from "./foo/bar"; // <- this is shorter so it will use this even though it's relative
    

    If that isn't working you may need to restart your ts server. You can do that by

    1. Opening a TS file
    2. Entering your command palette
    3. Searching "TypeScript: Restart TS server"

    Hopefully that helps.