react / react/metro

Including local packages from outside project root. Can we do better?

Open
#1,225 3 comments 61 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
JavaScript
Stars
5.6k
Forks
696
Avg merge
8m
Merged PRs (30d)
7

Description

Introduction

My goal is to find a solution to include packages which reside outside of the directory tree of a project. By "project," I refer
to a React Native directory that contains a package.json and node_modules generated using npx react-native init <name>.

Consider the application and package directories as follows:

/home/sam/apps/my-app
/home/sam/development/my-package

In this setup, my-app represents my React Native project, while my-package is my private UI component library. The objective is to incorporate my-package into my-app. my-package contains a package.json and some source files, typically located in the src directory.


Why?

The ability to store a package in a single location and share it across multiple projects offers a clean and efficient way to
manage code. It eliminates the need to synchronize every project that utilizes the package.

Solution and the problem

A common approach to achieve this is through the use of a monorepos. However, this may not always be the most preferred
solution. Curently, it appears taht employing a monorepo is the only viable option for sharing local packages between React
Native projects.

Another potential solution involves leveraging the nodeModulesPaths and/or extraNodeModules features of
metro.config.js. However, these features only function partially, and I'm in the process of investigating why and hopefully
devising a fix.

Consider the scenario where I have my own UI component library in /home/sam/development/my-package, which I intend to utilize in several React Native apps, including /home/sam/apps/my-app. You can include the path to my-package in extraNodeModules as follows:

const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');

/* ------------------------------------------------------- */

/* Path to `package.json` of my-package. */
let my_package_path = '/home/sam/development/my-package/';

const config = {
  resolver: {
    extraNodeModules: {
      'my-package': my_package_path,
    },
  },
  watchFolders: [
    my_package_path,
  ],
};

/* ------------------------------------------------------- */

module.exports = mergeConfig(getDefaultConfig(__dirname), config);

/* ------------------------------------------------------- */

However this approach only partially works. When my-package utilizes any React Native components, such as View in my case this results in the following error:

ERROR  TypeError: Cannot read property 'useContext' of null
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem. 
    at View (http://10.0.2.2:8081/index.bundle//&platform=android&dev=true&lazy=true&minify=false&app=com.myapp&modulesOnly=false&runModule=true:183707:43)
    at UiButton
    at RCTView
    at View (http://10.0.2.2:8081/index.bundle//&platform=android&dev=true&lazy=true&minify=false&app=com.myapp&modulesOnly=false&runModule=true:59759:43)
    at App
    at RCTView
    at View (http://10.0.2.2:8081/index.bundle//&platform=android&dev=true&lazy=true&minify=false&app=com.myapp&modulesOnly=false&runModule=true:59759:43)
    at RCTView
    at View (http://10.0.2.2:8081/index.bundle//&platform=android&dev=true&lazy=true&minify=false&app=com.myapp&modulesOnly=false&runModule=true:59759:43)
    at AppContainer (http://10.0.2.2:8081/index.bundle//&platform=android&dev=true&lazy=true&minify=false&app=com.myapp&modulesOnly=false&runModule=true:59601:36)
    at myapp(RootComponent) (http://10.0.2.2:8081/index.bundle//&platform=android&dev=true&lazy=true&minify=false&app=com.myapp&modulesOnly=false&runModule=true:110702:28)

There might be several reasons for this as you explained read here. My suspicion is that, in this specific case, it might be due to versioning discrepancies between react-native or react. Interestingly I couldn't find a version mismatch when using the following commands:

npm ls react
npm ls react-native
Workaround

To fix this, I found this note:

This will be done by deleting the react and react-dom folders
in the node_modules in the project you are developing.

After deleting the node_modules/{react, react-native} from my-package, we have to fix one more thing. Open metro.config.js of my-app and add the following fix for the removed react and react-native modules. To do this, make sure that react and react-native are also in the extraNodeModules. This is also mentioned here and
here.

After these changes the metro.config.js of my-app looks like:

const path = require("path");
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');

/* ------------------------------------------------------- */

/* Path to `package.json` of my-package. */
let my_package_path = '/home/sam/development/my-package/';

const config = {
  resolver: {
    extraNodeModules: {
      'my-package': my_package_path,
      'react': path.resolve(__dirname, 'node_modules/react'),
      'react-native': path.resolve(__dirname, 'node_modules/react-native')
    },
  },
  /* We also add the path to our watch folders so changes are followed */
  watchFolders: [
    my_package_path,
  ],
};

/* ------------------------------------------------------- */

module.exports = mergeConfig(getDefaultConfig(__dirname), config);

/* ------------------------------------------------------- */

🔥 Then, cleaning the cache and reinstalling the app onto your emulator or devices allows you to use a package from an external location. To clean run (or one of the variants) in the my-app directory:

npm start -- --reset-cache
Can't we do better?

As you can see using local packages is poorly supported although it feels to me something which should have worked from day one. Event the first issue created here is relevant. There are many partial, half working articles as you can see below.

What is holding us back to implement a fix for this? Who, with know-how about the internals of metro which makes this a difficult problem can share some thoughts on this?

Relevant issues and articles

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reproducing the external-package setup using metro.config.js with extraNodeModules and watchFolders, including the duplicate React failure described in the issue. Read the linked Metro issues about symlinks, resolution outside the project root, and package deduplication. Done would require a defined Metro behavior or implementation plan for reliably consuming external local packages without duplicate React or React Native modules.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, react-native
Domain
build-system, tooling
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.