Another React Native upgrade story
How we managed to upgrade a Lerna monorepo project to React Native 0.59 with Yarn workspaces.
You already know that a React Native upgrade is not an easy process. Depending from which version of react native you start to upgrade there are a lot of things that need to be changed, added or removed.
Each scenario can be different, that’s why we highly recommend to use rn-diff-purge. This tool helped us to see what we need to change to be able to use the latest version of React Native.
In this article we will focus more on some specific problems related to Lerna, Yarn Workspaces, Metro bundler and symlinks.
Here are some stories about the most difficult things that we encountered during the upgrade.
From Lerna to Yarn Workspaces
Lerna is a great tool to manage monorepos, besides the lerna bootstrap command we didn’t use any of its features, so we thought that yarn workspaces will be a better fit for our needs because it was simpler and it’s more mature now.
With lerna, our monorepo project was looking like this:
app
packages
app-native
app-shared
app-web
package.json
lerna.json
After removing lerna we obtained a much cleaner look. The new project structure was looking like this:
app
node_modules
packages
app-native
app-shared
app-web
package.json
We don’t have a lerna.json file anymore and a new node_modules directory has showed up. That directory will contain all the shared dependencies.
Yarn workspaces tries to optimize the node_modules directory by sharing common node modules between projects, in this way if app-native has the same dependency as the app-shared. That dependency will be moved in the node_module directory from project’s workspace and a symlink will be created & added in node_modules directory from each project. This process is called hoisting.
React Native and Symlinks
We all know that React Native does not support symlinks, this was a big problem because our monorepo packages were not recognized by the React Native packager. Fortunately yarn workspaces has a way to define which packages to be hoisted and which not.
First thing was to tell yarn workspaces to ignore the react-native package. This can be done by adding in the package.json file from app workspace a nohoist property with the following rules:
"nohoist": [
"**/react-native",
"**/react-native/**",
"**/*react-native*",
"**/*react-native*/**"
]
Let’s explain each hoisting rule.
This rule entry:
"**/react-native",
tells to yarn workspaces not to hoist the react-native package no matter where it is.
This is called shallow hoisting, because it will only no hoist only the react-native package but its dependencies will be hoisted. To resolve that, the second line comes into play:
"**/react-native/**",
It will do a deep no hoist, this means that all the dependencies related to the react-native package will not be hoisted.
Next step was to do the same thing for all react-native dependent packages:
"**/*react-native*",
"**/*react-native*/**"
These two lines will do a shallow and deep no hoisting for all package names that contain the “react-native” characters.
Metro and Symlinks
Another big challenge was to make metro to look for changes in all packages that depend to a specific package. For example the app-native project had most of the backend code from app-shared project. The problem was that metro didn’t know to look for code changes in both projects. More specific, only the app-native project was watched for code changes.
We fixed this by telling to metro packager to watch the rest of the monorepo packages using a metro.config.js file.
In this file we used a development node packages called get-dev-paths, this package will return the node modules which are symlinks and their real path it’s not in another node_modules directory. It was perfect for us, because the app-shared package is a symlink for app-native but its real path is in app/packages and not in some other node_modules directory.
const fs = require("fs");
const getDevPaths = require("get-dev-paths");
const projectRoot = __dirname;
module.exports = {
watchFolders: Array.from(
new Set(getDevPaths(projectRoot).map($ => fs.realpathSync($)))
),
resolver: {
blacklistRE: /app-shared\/node_modules\/react-native\/.*/
},
};
Another problem was that react-native was used in both app-native and app-shared, ideally react-native shouldn’t be present in app-shared project but if your project shares the navigation logic and it uses react-navigation for that, react-navigation use react-native as a dependency.
This was causing an error in the metro bundler because it didn’t know which react-native package to import.
These lines fix the error this by telling metro to ignore the react-native package from app-shared project.
resolver: {
blacklistRE: /app-shared\/node_modules\/react-native\/.*/
},
Doing this we managed to obtain a fully working monorepo project using yarn workspaces, React Native 0.59, Babel 7, FlowType 0.92, Xcode 10.2.1 and Android Gradle 3.3.
React Native has a great community behind and that inspires us to share and build better products, we hope that we helped to fix some of the problems you encountered during the upgrade process.
Comments