metro
metro copied to clipboard
Including local packages from outside project root. Can we do better?
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
- Linking Local Packages in React Native the Right Way
- Linking local packages in React Native using NPM Link and Haul
- Install local packages as dependency with react-native
- How to improve local imports in React Native
- Npm link doesn't work with React Native, what do you use for testing local modules
- metro-resolver-symlinks
- wml
- Follow symlinks
- Modules required from outside of root directory does not find node_modules
- With symlinked packages: Error unable to resolve ...
- Package deduplication
- unable to resolve path outside root directory
- I can't use a symlinked package
- feat: symlinks in node_modules
- Asset paths (httpServerLocation) invalid when assets is in watchFolders