12 KiB
title, description, published, authors, tags, attached, license
| title | description | published | authors | tags | attached | license | |||
|---|---|---|---|---|---|---|---|---|---|
| How to Setup a React Native Monorepo | 2023-05-05T13:45:00.284Z |
|
|
cc-by-nc-sa-4 |
React Native allows you to write React code that outputs to native applications for various platforms, including:
- Android
- iOS
- Windows
- macOS
It's an undeniably powerful way to share code between web applications and your mobile apps; particularly within small teams that either don't have the knowledge or the capacity to go fully native.
Similarly, monorepos can be a fantastic way to share code between multiple projects with a similar tech stack.
Combined together and even a small team can maintain multiple React Native applications seamlessly.
Unfortunately, it can be rather challenging to build out a monorepo that properly supports React Native. While Expo supports monorepo usage, one common complaint when using Expo is that Expo does not support many popular React Native libraries that require native code.
To further exacerbate the issue, React Native comes with many uncommon edgecases that makes monorepos particularly challenging to create. Many of the tutorials I've found outlining how to build a monorepo for this purpose use outdated tools to work around this.
Knowing just how potent the potential impact of a monorepo would be to my projects, I disregarded these headaches and spent a month or two building out a monorepo that solved my problems.
By the end of it all, I had a monorepo structure that looked something like the following:
apps/customer-portal/{open: false}android/ios/srcApp.tsxcomponents/hooks/utils/types/
.eslintrc.jsapp.jsonbabel.config.jsindex.jsmetro.config.jsnode_modulespackage.jsontsconfig.json
admin-portal/{open: false}android/ios/srcApp.tsxcomponents/hooks/utils/types/
.eslintrc.jsapp.jsonbabel.config.jsindex.jsmetro.config.jsnode_modulespackage.jsontsconfig.json
packages/config/{open: false}.eslintrc.jsbabel-config.jseslint-preset.jspackage.jsontsconfig.json
shared-elements/{open: false}src/components/hooks/utils/types/
.eslintrc.jspackage.jsonvite.config.ts
.eslintrc.js.gitignore.yarnrc.ymlREADME.mdpackage.jsonyarn.lock
I'd like to share how you can do the same in this article. Let's walk through how to:
Setup React Native Project
Let's setup a basic React Native project to extend using a monorepo.
Before you get started with this section, make sure you have your environment set up, including XCode/Android Studio.
To setup a basic React Native project from scratch, run the following:
npx react-native init CustomerPortal
Once this command finishes, you should have a functioning React Native project scaffolded in CustomerPortal folder:
android/ios/.eslintrc.jsapp.jsonApp.tsxbabel.config.jsindex.jsmetro.config.jsnode_modulespackage.jsontsconfig.json
We now have a basic demo application that we can extend by adding it to our monorepo.
Maintain Multiple Package Roots with Yarn
In a monorepo, however, we might have multiple apps and packages that we want to keep in the same repository. To do this, our filesystem should look something akin to this structure:
apps/customer-portal/android/ios/srcApp.tsxcomponents/hooks/utils/types/
.eslintrc.jsapp.jsonbabel.config.jsindex.jsmetro.config.jsnode_modulespackage.jsontsconfig.json
admin-portal/android/ios/srcApp.tsxcomponents/hooks/utils/types/
.eslintrc.jsapp.jsonbabel.config.jsindex.jsmetro.config.jsnode_modulespackage.jsontsconfig.json
packages/config/.eslintrc.jsbabel-config.jseslint-preset.jspackage.jsontsconfig.json
shared-elements/src/components/hooks/utils/types/
.eslintrc.jspackage.jsonvite.config.ts
.eslintrc.js.gitignore.yarnrc.ymlREADME.mdpackage.jsonyarn.lock
Notice how each of our sub-projects has it's own package.json? This allows us to split out our dependencies based on which project requires them, rather than having a single global package.json with every project's dependencies in it.
To do this, we need some kind of "workspace" support, which tells our package manager to install deps from every package.json in our system.
Here are the most popular Node package managers that support workspaces:
While NPM is often reached for as the default package manager for Node apps, it lacks a big feature that's a nice-to-have in large-scale monorepos: Patching NPM packages.
While NPM can use a third-party package to enable this functionality, it has shakey support for monorepos. Compare this to PNPM and Yarn which both have this functionality built-in for monorepos.
This leaves us with a choice between pnpm and yarn for our package manager in our monorepo.
While pnpm is well loved by developers for it's offline functionality, I've had more experience with Yarn and found it to work well for my needs.
Using Yarn 3 (Berry)
When most people talk about using Yarn, they're often talking about using Yarn v1 which originally launched in 2017. While Yarn v1 works for most needs, I've ran into bugs with its monorepo support that halted progress at times.
Here's the bad news: Yarn v1's last release was in 2022 and is in maintainance mode.
Here's the good news: Yarn has continued development with breaking changes and is now on Yarn 3. These newer versions of Yarn are colloquially called "Yarn Berry".
To setup Yarn Berry from your project, you'll need:
- Node 16 or higher
- ... That's it.
While there's more extensive documentation on how to install Yarn on their docs pages, you need to enable Corepack by running the following in your terminal:
corepack enable
Then, you can run the following:
corepack prepare yarn@stable --activate
Disabling Yarn Plug'n'Play (PNP)
https://yarnpkg.com/features/pnp#incompatible
It's worth mentioning that while PNPM doesn't use PNP as its install mechanism, it does extensively use symlinks for monorepos. If you're using PNPM for your project, you'll likely want to disable the symlinking functionality for your monorepo.
A note about nohoist
https://twitter.com/larixer/status/1570459837498290178
Package Shared Elements using Vite
Fixing issues with the Metro Bundler
// eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require("path");
/**
* @param {string} __dirname
*/
module.exports = (__dirname) => {
const packagesWorkspace = path.resolve(path.join(__dirname, "../../packages"));
const watchFolders = [packagesWorkspace];
const nodeModulesPaths = [path.resolve(path.join(__dirname, "./node_modules"))];
return {
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: true,
inlineRequires: true,
},
}),
},
resolver: {
resolveRequest: (context, moduleName, platform) => {
if (moduleName === 'react') {
return {
filePath: path.resolve(path.join(__dirname, "./node_modules/react/index.js")),
type: 'sourceFile',
};
}
if (moduleName === 'react-native') {
return {
filePath: path.resolve(path.join(__dirname, "./node_modules/react-native/index.js")),
type: 'sourceFile',
};
}
// Optionally, chain to the standard Metro resolver.
return context.resolveRequest(context, moduleName, platform);
},
nodeModulesPaths,
},
watchFolders,
};
}
Better Method
// eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require("path");
/**
* @param {string} __dirname
*/
module.exports = (__dirname) => {
const packagesWorkspace = path.resolve(
path.join(__dirname, "../../packages")
);
const watchFolders = [packagesWorkspace];
const nodeModulesPaths = [
path.resolve(path.join(__dirname, "./node_modules")),
];
return {
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: true,
inlineRequires: true,
},
}),
},
resolver: {
resolveRequest: (context, moduleName, platform) => {
if (
moduleName.startsWith("react") ||
moduleName.startsWith("@react-navigation") ||
moduleName.startsWith("@react-native") ||
moduleName.startsWith("@react-native-community") ||
moduleName.startsWith("@tanstack") ||
moduleName.startsWith("styled-components") ||
moduleName.startsWith("@redux") ||
moduleName.startsWith("redux")
) {
const pathToResolve = path.resolve(
__dirname,
"node_modules",
moduleName
);
return context.resolveRequest(context, pathToResolve, platform);
}
// Optionally, chain to the standard Metro resolver.
return context.resolveRequest(context, moduleName, platform);
},
nodeModulesPaths,
},
watchFolders,
};
};
