Pitfalls of Building a Monorepo for React Native and React Web Apps
The focus of this article and video is on the pain points you may encounter while combining React Native (RN) and web-based Create React App (CRA) with TypeScript in a single monorepo.
Table of Contents
The focus of this article and video is on the pain points you may encounter while combining React Native (RN) and web-based Create React App (CRA) with TypeScript in a single monorepo. You could google it, but you’ll just end up creating more work for yourself. When I tried, Google returned over 20 hits containing various workarounds—most of which weren’t compatible with the latest version of RN at the time.
If you don’t have the exact same setup that’s in the video, don’t fret. There are several approaches which can be applied to different setups, whether you have a stand-alone (CRA) or a monorepo with different CRAs.
DEMONSTRATION
The video is a step-by-step guide to setting up the monorepo on a simple, practical example. Various challenges are described with an explanation of how each one was solved. You will see how to build a small monorepo which contains the following:
- RN app (w/o Expo)
- React web app – CRA
- “Theme” shared folder
- “Core” shared folder
- “Types” shared folder
MONOREPOS
A monorepo is a single GitHub repository where you can have multiple projects, each with a separate set of dependencies (i.e. a standalone package with its own package.json
file). You can combine all these child projects into one repo to make them easier to maintain. This kind of structure is beneficial for NPM workflows when you have multiple packages that you are regularly publishing.
It is most beneficial if some packages share common dependencies (e.g. if your "web" and "mobile" packages both use a "core" package). When you create a pull request (PR) in GitHub, it contains all the changes in all the packages in one place. You don’t have to manage separate repositories and do complicated continuous integration, versioning, and management.
Pros
- Ease of version control management
- Easier to keep everything in sync
- Single place for configs
- Visibility of an entire project's codebase
- Easier cross-package changes: one git PR instead of orchestrating PRs in different repos
- Best used when grouping related packages and their shared dependencies
- Large-scale refactoring is easier. For example, if you change the name of a constant that is shared between your web app and your server, you can immediately see all the effects of the change in your IDE.
Cons
- More complicated CI/CD setup (but seamless once you get it right). Most tools for RN don’t have sufficient support for monorepos.
- No way to restrict access only to some parts of the app. Normally, when you have separate repos for each package (e.g. a separate repo for the API and the client), you can specify different access rights for each one. If everything is in one repo, it’s more complicated.
- If you are just working on the RN app, you have to check out the whole monorepo, which takes up more disc space compared to just checking out the RN repo.
YARN WORKSPACES
What is it and why should I use it?
According to classic.yarnpkg:
Your dependencies can be linked together, which means that your workspaces can depend on one another while always using the most up-to-date code available. This is also a better mechanism than yarn link
since it only affects your workspace tree rather than your whole system.
All your project dependencies will be installed together, giving Yarn more latitude to better optimize them.
Yarn will use a single lockfile rather than a different one for each project, which means fewer conflicts and easier reviews.
How does Yarn do this?
Yarn does this through a process called hoisting. Yarn will hoist common dependencies from each package of your repo to the workspace root node_modules
, creating a single yarn.lock
file. This also saves you disk space.
How does it work with a monorepo?
Compared to a normal project where you have one repo with one package, in a monorepo we have several packages and each can have its own set of dependencies.
Let’s say package-1 is your API and package-2 is your RN app. Both have a shared dependency called B (1.0). When you run yarn install
in this monorepo, it will move B (1.0) to the root (on the right side). Then it will create a symlink to your child packages so they don’t have to have the package installed in their node_modules
.
This can save you a tremendous amount of disk space when you are working on a large monorepo. This is great but can cause some problems as discussed in the video.
What should be a package in a monorepo?
The general guideline is that, from the start, you should only make packages for things which will be released or versioned separately or have their own dependencies. In our case that would be the web app, the mobile app, and the API.
When it comes to "theme", we don't want to release or version it separately. At this point, we’re not sure if it will have any dependencies. It’s better to treat it as a simple shared folder and turn it into a package only if and when we need to.
The reason for this is that packages require additional orchestration compared to simple shared folders. For example, packages need to be versioned and unlike shared folders, you don't get hot-reloading out of the box.
LERNA
Lerna is a tool which was created before Yarn Workspaces existed. It has similar goals—installing your dependencies to the root folder of the monorepo and symlinking them to the child packages—but Lerna uses NPM instead of Yarn.
You can use Lerna together with Yarn and Yarn Workspaces. This lets you use Yarn workspaces as a low-level tool for symlinking and Lerna just for high-level orchestration. Lerna gives you useful commands like lerna run
that can automatically execute a script in all the child packages.