Monorepos with Next.js and React Native
Not long ago I needed to figure out how to share code efficiently between an existing Next.js project and a newly created React Native project. In this note I'll describe the particulars of our scenario, the options available and the solution implemented.
We decide to initialize a new subdirectory in our repo and write a custom Metro configuration to allow our React Native project to access files outside its root directory. No special yarn/git features or extra tools to manage. Not beautiful, but very functional. Read on for more details!
Our repo has a Next.js application that lives at the root alongside directories for infrastructure, database migrations, tests and shared code. It looks something like this:
our-lovely-repo └─ components └─ features └─ infra └─ lib └─ migrations └─ pages └─ styles └─ tests
Where do we introduce a React Native project if we want to share code with our Next.js project?
There are two major approaches to repo organization:
- Classic - Each project in its own repo sharing code via private npm packages, (probably) managed with a tool like Lerna
- Monorepo - A single repo with projects as subdirectories
To (over)simplify things a bit, the tradeoff is between structure and speed. With the classic multi repo approach, you have a rigid structure that has more steps and formalism to manage package updates, and as such is usually slower. On the other hand, a monorepo structure can enable faster updates and reduce friction with the right management -- but what is the right approach to management? There are a number of tools, each with their own sets of tradeoffs:
In our scenario we prefer speed above all else, so a monorepo is the solution we lean towards. Before exploring options to make that happen, we should first consider whether Next.js itself is amenable to the idea of living in a monorepo.
A Next.js gotcha
Sensible software engineers that we are, it is tempting to look at the repo structure above, isolate our Next.js project related directories, and initialize a new repo for our React native project:
our-lovely-repo └─ infra └─ lib └─ migrations └─ native ├─ components ├─ features ├─ screens └─ test ├─ integration └─ smoke └─ utils └─ styles └─ web ├─ components ├─ features ├─ pages └─ test ├─ integration └─ smoke └─ utils └─ styles ...
Looks clean right? The thing is, Next.js really does not want to live in a subdirectory.
To borrow some links from Joseph Luck's excellent monorepo solution, Next.js is very opinionated about project structure, making monorepos containing Next.js projects very painful:
- Next.js #5666 - Files outside of app directory will not be transpiled
- Spectrum - Accessing code outside Next.js project root
- Spectrum - Next.js monorepo example request
- Spectrum - Next.js monorepo styles transpilation issue
Joseph's solution is exciting in that someone finally wrote up a working solution for how to get Next.js projects working well in a monorepo structure, but has some drawbacks:
- Requires next-transpile-modules - which links our entire repo structure to a single community-supported node module
- Requires a
postinstallscript to enable hot reloading
That being said, there might be a middle ground we can explore to avoid upsetting Next.js.
The solution we eventually landed on is the simplest: leave everything where it is, and create a new subdirectory to house the React Native project:
our-lovely-repo └─ components └─ features └─ infra └─ lib └─ migrations └─ native ├─ components ├─ features ├─ screens └─ test ├─ integration └─ smoke └─ lib └─ styles └─ pages └─ styles └─ tests
While this structure is not the most ideal since there is no clear separation between project related files, it does allow us to avoid adding any tools or processes and still achieve a monorepo structure. Git performance, although something to consider in the long run, is not an issue at this scale. There is, however, one remaining challenge with this approach: how to share code outside the root of the directory the React Native project lives in.
Metro extra node modules config
Imagine we're in a component in our React Native project and we want to use a util in the parent
// In our-lovely-repo/native/screens/home.tsx ; ; // The bundler throws an error, can't find the util ;
The bundler does not know how to access the
capitalize module in the top level
lib directory. To solve this, we need to tell Metro where to find it:
// In our-lovely-repo/native/metro.config.js ; ; // Default expo config to merge ; // Enable access to `lib` top level directorh ; module.exports =;
This config uses a Proxy that does a few things:
- Defines extra paths to node modules outside the project root, in this case the top level
- Describes where the top level
node_modulesdirectory is in case a module in
libimports a module from there
In addition, we use an
@olr alias (our-lovely-repo) to differentiate between the top level
lib directory and the
lib subdirectory in
How do we enable this alias?
TypeScript paths config
Given that we are using TypeScript in our scenario, we need to tell TS that we want to use the
@olr alias and where it should look for files when we use the alias.
// In our-lovely-repo/native/tsconfig.json
With that done, we can now update our import to use the newly defined path alias:
// In our-lovely-repo/native/screens/home.tsx ; ; // The bundler is happy, and so are we! ;
And there we have it! While the solution we decided on may certainly not be appropriate for other use cases, this functional but ugly approach helped us avoid writing a thesis on proper monorepo solutions (🧐) and start scaffolding our React Native project right away.
Thanks for reading, and good luck with your next monorepo! 😄
Thanks for reading! Go home for more notes.