React Native — Monorepos & Code Sharing

Thibault Malbranche
Brigad Engineering
Published in
6 min readJan 13, 2020

--

Let’s examine everything that might go wrong when trying to use React Native in a monorepo. Then, let’s fix it. 🎉

But first… what’s a monorepo?

Monorepos allow you to have multiple projects share common dependencies instead of installing the dependencies for each of them.
They also simplify sharing code between your projects, allowing you to import code from one package to another.

As an example, you might want to have a monorepo containing a website, a mobile application, and some shared code that you wrote.

One of the most popular options to do that is yarn workspaces — which means that, yes, It’s already built into your favorite package manager! 😄

React Native x Yarn Workspaces
Save up some space by sharing node_modules

Great! Let’s use it! What could go wrong? 😃

While moving all the node_modules sounds like a great way to save so space, your dependencies code might end up somewhere where React Native struggles to find it. 😕

Let’s dive into the configuration adjustments you will need for your project to work perfectly.

Please note that this article was written while targetting React Native 0.61.5 and using React Native CLI 3.0.4, and configuration has been greatly simplified by the recent changes in the ecosystem. 💪

Setting up the monorepo

Following the yarn docs, if migrating from existing apps or creating a new project, add all your apps to a packages folder inside your monorepo, and run yarn init at the root.

Edit your package.json to add the workspaces field. You can either be specific or use globs. Don’t forget to run yarn!

Package.json modifs
Adding the workspaces field to package.json

Optional step: Move all your config (prettier, eslint and others) to the top-level packages.json dependencies. This enforces the same coding style across your whole codebase, neat!

Understanding what’s happening to your node_modules

Disclaimer: This is my understanding of what’s happening with the yarn workspaces node_modules management, let me know if some things are incorrect.

Think of the workspaces resolution algorithm as the way to save to most space possible on your project.

By default, any dependency will be “hoisted” (placed at the top-level). This allows installing dependencies only once even if they are used in multiple projects. Then it has to deal with version conflicts, the best way to save space is to share the versions that have the biggest number of packages needing it.
(You can enforce this via yarn resolutions)

This means that you should try to keep the same versions across your packages if you want to save space and workspace complexity for yarn because bumping some dependency might move many modules in the workspace. (I remember once forgetting to update @types/react and having all the react-related dependencies moving down to the packages)

But in the end, you cannot really guarantee that some dependencies will stay somewhere, and that’s fine, this is how it’s supposed to work. (You can force a dependency to stay inside a package by using no-hoist, but that should be avoided as it makes yarn workspaces kindof useless 😅)

Pro-tip: if you want to look up all the versions and install directories of a dependency you can run “yarn why dependency”.

Back to React Native, shall we?

React Native has greatly simplified the use of monorepos recently thanks to Mike Grabowski, and many others helping with the new CLI, but there are still a few quirks we need to take care of. But know that we are committed to making this as seamless as possible!

Solving issues in native files

As a first step, we need to update paths to React Native and the cli if it has moved from your packages to the top of your monorepo because we use it in many native files, including Podfile, project.pbxproj, build.gradle, and settings.gradle. Our main issue here is that we would need to update the path every time it moves around in the monorepo.

Remember when I said: “you cannot really guarantee that some dependencies will stay somewhere”. There is another way than no-hoist to force a dependency to be somewhere. If you add a dependency to the workspace root, it will have no choice but to be at the top, so what I like to do to make sure React Native will have a consistent path in my node_modules is adding it to the top-level package.json. (Using “*” as the version number aligns it to the one in your packages)

Forcing React Native to the top-level of a workspace

Please note that if you choose to do this, it’s best to keep all your packages to the same React Native version, or you might end up using the wrong version in your project. ⚠️

Now that we know where React Native will be, we can update all the files referencing it. (I’m considering that React Native is hoisted at the root in my examples, but feel free to use the path where it’s installed for you)

If your React Native is still inside the packages folder node_modules, you maybe only need to update the CLI paths.

ios/Podfile
android/app/build.gradle
android/build.gradle
android/settings.gradle

Then you need to edit your project.pbxproj file, I would advise using your normal IDE (VSCode here) to prepend “../../” where needed.

Replace paths inside pbxproj (screenshot before replacing, you probably want to add “../../”)

Then in the “Bundle React Native code and images” step, you want to add an export to specify the PROJECT_ROOT. I used XCode to edit it but use the tool you prefer.

Bundle React Native code and images

Once you updated everything, do not forget to run pod install.
Since the V3 of React Native CLI, it will use Node resolution algorithm meaning that it will correctly find native dependencies even if they are hoisted. 🎉

Congrats you are done! For the native part 😏

Making sure Metro ❤️ monorepos!

Let’s get our favorite bundler to build properly, let’s edit metro.config.js and add the watchFolders field:

Adding watchFolders

So now Metro is able to find all of our code! Great 😃

If you have multiple versions of React Native installed in your workspace, you want to exclude those of the haste module resolution. My workaround here is to use these few extra lines inside my metro config. What they do is exclude any react-native folder except for the one we need. Let’s use blacklistRE !

Adding blacklistRE

Another issue for android is that assets resolutions will not work unless you use another workaround. If you plan on having shared assets (jpg, png, etc…), you need to add yet another few lines of dark magic. 🔮
You want to add publicPath and enhanceMiddleware. Please note that in the example you can prefix it by anything, but you need at least as many depths in your fake path that in your monorepo. See issue!

Adding publicPath & enhanceMiddleware

Last step before enjoying the sweet monorepo life

This is not unique to React Native but some issue that you will often have is that some libraries will not work well at all with workspaces if you import some code from two different versions, or even from the same version installed in two places. (👋 React, React Navigation, React Native SVG, @apollo/react-hooks, apollo-client and many more…)
For example, see https://reactjs.org/warnings/invalid-hook-call-warning.html#duplicate-react.

To fight against this you can use babel and the module-resolver plugin.
Inside my config, I force for a few known sensitive packages to be always resolved only one version, the one Node resolution algorithm would have found from the package repository. (Of course, you can add only your packages here!) 🐧

Partial babel config

Congratulations if you made it till the end, now what?

If you want to share some code between two packages you can include one package as a dependency in the package.json of another and it will just work. Even with native code!

You can find a working demo here.

As always, I want to thank everyone that ever shared tips and tricks, as that is mostly just an aggregation of a lot of others solution that I remixed together and tried to simplify. Writing down all the current workaround needed is the first step to fixing those issues 😅.

Have fun and if needed you can reach me on twitter. 🚀

--

--

Lead front-end mobile at @joinbrigad in Paris. I ❤️ open-source. Part of the React Native Core contributors. Twitter / Github: @titozzz — DM opens! 🚀