When working on more than one React Native project, it makes sense at some point to extract shared tools into an external library.
We then reference the shared library from all projects, and maintain a single codebase for the shared code. For whatever reason, the Metro bundler used by React Native does not support symlinks, so we need a hack.
I’ve found two ways to do this:
- Work on the library externally, and directly require the source in the React Native project (import sharedlib from ‘../../test/some-shared-code’
- Work on the library externally, and require the shared library in package.json ‘import sharedlib from ‘sharedlib’. For dev purposes, rsync the changes over.
This post covers working on the library externally, and requiring via rn-cli.config.js
Work on the library externally
In React Native, this takes some massaging to work. The Metro bundler doesn’t allow symlinks, so we need to add the external root to the Metro config.
This approach has issues when working on a library that will be used in multiple projects, and when worked on by others. It complicates project setup and isn’t an elegant way to present your project.
Example rn-cli.config.js, pre babel 7 (< RN 0.57)
Shared library is in ./shared-library. :
const Path = require('path');
const blacklist = require('metro').createBlacklist;
module.exports = {
getTransformModulePath() {
return require.resolve('react-native-typescript-transformer');
},
getBlacklistRE: function () {
return blacklist([]);
},
extraNodeModules: {
react: Path.resolve(__dirname, "node_modules/react"),
"react-native": Path.resolve(__dirname, "node_modules/react-native")
},
getSourceExts() {
return ['ts', 'tsx'];
},
getProjectRoots: () => [__dirname, Path.join(__dirname, "../shared-library")]
}
Note specifically ‘getProjectRoots’, where we tell the bundler to check in the shared-library folder as well.
Also note ‘extraNodeModules’, where we tell the bundler which dependencies to share with the shared-library when adding it.
‘getBlacklistRE’ is an array of files to ignore from the current project or included project.
Example rn-cli.config.js, babel 7 (RN 0.57+)
const Path = require('path');
module.exports = {
resolver: {
sourceExts: ['tsx', 'ts', 'js'],
extraNodeModules: {
react: Path.resolve(__dirname, "node_modules/react"),
"react-native": Path.resolve(__dirname, "node_modules/react-native")),
}
blacklistRE: /shared-library\/node_modules\/react-native\/
},
transformer: {
babelTransformerPath: require.resolve("react-native-typescript-transformer")
},
watchFolders: [Path.join(__dirname, "../shared-library")]
};
Note the different syntax.
Why this method sucks
Linking to your library from the app with rn-cli.config.js works perfectly fine. The Metro bundler does what it says on the box, the file system watcher works, live reloading still works fine.
The problems appear down the track. In my case, I had two apps making use of a shared helper library. I’d work on app 1 for a few weeks, and then app 2 for a few weeks. Sometimes there’d be an update to the helper library.
Next time I dig out app 1 to work on it, there’s uncertainty about what’s actually changed in the shared library. Something has broken, a test is failing, a method has moved. When functionality was being improved or added for app 1, app 2 was coming along for the ride whether I liked it or not.
There’s no real way to pin each app to a stable version as development continues, and the CI/CD pipeline became just as brittle – if I re run a build next week, and I’ve made a commit to the shared library in the meantime, the build is a different output. Big no no.
What’s the solution?
I’ll cover this in a better post soon, but the solution to this problem is to require the shared library in the package.json dependencies of the app projects.
During development, the library is rsynced into the node_modules folder during development. You’re able to work on and debug the shared library and the app codebase at the same time.
When the library is finished being worked on, commit, push, and pin the dependency version in the app to the commit hash.
rm -rf node_modules, npm install, and now the app is pinned to the last *actual version* of the shared library you ran the app against.
The only shortcoming I’ve found with this method so far is it requires discipline/learning to make sure you’re working on the source file (in the shared library project) and not the destination file (in the app project/node_modules/shared-library folder).