Search code examples
typescriptreact-nativebabeljstsconfigmetro-bundler

How to use path alias for a folder outside project root in React Native?


I'm working on a React Native project that's part of a wider project with multiple packages. The folder structure of the project is as follows (excluding most of the irrelevant files):

monorepo
└── packages
    ├── backend
    ├── frontend-app
    │   ├── babel.config.js
    │   ├── metro.config.js
    │   ├── tsconfig.json
    │   ├── index.js
    │   └── src
    └── shared
        └── data-types.ts

Inside the frontend-app folder, I want to be able to import types from the data-types.ts file. Additionally, I want to do this by using @shared/data-types rather than having to use a relative path ../../shared/data-types (or even more folders up).

I was able to find only that you have to update the babel.config.js and tsconfig.json for regular path aliases inside the app project itself, but when running the app I still get a warning like this:

Unable to resolve module ../../../../shared/data-types from ***: 

None of these files exist:
  * ../shared/data-types(.native|.ios.js|.native.js|.js|.ios.jsx|.native.jsx|.jsx|.ios.json|.native.json|.json|.ios.ts|.native.ts|.ts|.ios.tsx|.native.tsx|.tsx)
  * ../shared/data-types/index(.native|.ios.js|.native.js|.js|.ios.jsx|.native.jsx|.jsx|.ios.json|.native.json|.json|.ios.ts|.native.ts|.ts|.ios.tsx|.native.tsx|.tsx)

[...]

How to make it work for files outside of the React Native project's folder?


Solution

  • Path aliases are incredibly useful for cleaner imports and avoiding relative path hell in larger projects. While they're commonly used within a project, sometimes there's a need to alias paths outside the React Native project root, especially in monorepos or when sharing code across multiple projects.

    To make path aliases work for files/folders outside of the React Native app's project root, you have to modify the babel.config.js, tsconfig.json, and the metro.config.js files.

    Given the directory structure where your React Native project and the shared folder are sibling directories (i.e., they have the same parent directory), here's how you can set up the path alias:

    1. Install the required dependency: First, you need to add a development dependency to your React Native project to help Babel resolve the aliases.

    Using npm:

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

    Or using yarn:

    yarn add --dev babel-plugin-module-resolver
    

    2. Configure your files:

    babel.config.js

    module.exports = {
      presets: ["module:metro-react-native-babel-preset"],
      plugins: [
        "react-native-reanimated/plugin",
        [
          "module-resolver",
          {
            extensions: [".ios.js", ".android.js", ".ios.jsx", ".android.jsx", ".js", ".jsx", ".json", ".ts", ".tsx"],
            root: ["."],
            alias: {
              "@shared": "../shared",
            },
          },
        ],
      ],
    };
    

    tsconfig.json

    {
      "compilerOptions": {
        // Other settings
        "baseUrl": "./" /* Base directory to resolve non-absolute module names. */,
        "paths": {
          "@shared/*": ["../shared/*"]
        },
        // Other settings
      },
      "include": ["./src/*", "../shared/*"]
    }
    

    metro.config.js

    const { getDefaultConfig, mergeConfig } = require("@react-native/metro-config");
    const path = require("path");
    
    /**
     * Metro configuration
     * https://facebook.github.io/metro/docs/configuration
     *
     * @type {import('metro-config').MetroConfig}
     */
    const config = {
      watchFolders: [path.resolve(__dirname + "/../shared")],
    };
    
    module.exports = mergeConfig(getDefaultConfig(__dirname), config);
    

    Once you've made these changes, make sure to restart the Metro bundler. If you encounter any issues, resetting the cache can help: npx react-native start --reset-cache.

    With these changes, you'll be able to seamlessly import from the @shared alias in your React Native app.

    You can present this additional information as an update or a postscript to your original answer. Here's a possible format:


    Update:

    After implementing the above, I discovered that while everything works perfectly in development, local builds, and archives, there was an issue when trying to build using Fastlane on a remote machine (specifically, Azure DevOps pipelines). The error manifested during the build phase 'Bundle React Native code and images', resulting in a Fastlane exit code 65. The underlying error in the xcodebuild logs pointed to: "Unable to resolve module @babel/runtime/helpers/interopRequireDefault" within the shared package. This error hinted that the node_modules could not be located within the shared package.

    Thankfully, I stumbled upon this StackOverflow answer that provided a solution. The gist of the solution was an update to the metro.config.js. Note, this solution may not be necessary for everyone, but it resolved my specific issue.

    const { getDefaultConfig, mergeConfig } = require("@react-native/metro-config");
    const path = require("path");
    
    /**
     * Metro configuration
     * https://facebook.github.io/metro/docs/configuration
     *
     * @type {import('metro-config').MetroConfig}
     */
    const extraNodeModules = {
      shared: path.resolve(__dirname + "/../shared"),
    };
    
    const watchFolders = [path.resolve(__dirname + "/../shared")];
    
    const nodeModulesPaths = [path.resolve(path.join(__dirname, "./node_modules"))];
    
    const config = {
      resolver: {
        extraNodeModules,
        nodeModulesPaths,
      },
      watchFolders,
    };
    
    module.exports = mergeConfig(getDefaultConfig(__dirname), config);