This article explains a coexisting of react native and react web apps with similar to monorepo workspaces behavior, but without sharing node_modules
dependencies.
All web workspaces remain workspaces, mobile just acts like workspace.
I've had an existing huge web monorepo. Yarn's nohoist
setup, or editing metro configuration wasn't working for me because this monorepo contains a lot of dependencies incompatible with react native. But it might work for you and might be a better solution.
TL;DR
- You need to update metro config to handle sources outside of react native project.
- Setup mobile project's babel config with aliases to shared folders.
- Enjoy the magic ✨
Minimal working example [repo]
This article expects that you know how to create yarn workspaces monorepo. If not, please, read docs here.
Also, I have example repo with mobile and web sharing some code.
1. Create folder which needs to be shared
Let's say, we have our workspaces in the folder projects
, like this:
├── projects
│ ├── web
│ └── ... (means here can be lots of files)
├── package.json
...
Add a new workspace, which will contain shared files for both mobile and web.
├── projects
+ | ├── shared
+ | | ├── src
+ | | ├── index.js
+ | | └── package.json
│ ├── web
│ └── ...
├── package.json
...
// package.json
{
"name": "shared",
"scripts": {
...
},
"dependencies": {
...
}
}
// index.js
export { sayHello } from './src/sayHello'
// src/sayHello.js
export function sayHello() {
return 'Hello world!';
}
2. Add shared
to other workspaces
Now we need to add our shared folder into package.json
files of other workspaces.
// web/package.json
{
"name": "web",
"scripts": {
...
},
"dependencies": {
+ "shared": "*"
...
}
}
and same of all other workspaces, where we want to use shared
.
Then, to let yarn know what we did, run yarn install
or just yarn
.
Now we can do something like this:
// projects/web/src/somefile.ts
import { sayHello } from "shared";
sayHello();
3. Create react native application
React Native setup guide does a better job at explaining how to set up a new mobile app, or maybe you have your existing one. I'll skip this part, if you're not sure how to do this, docs are here.
You need to create a mobile app inside projects
folder like this:
└── projects
├── shared
| ├── src
| ├── index.ts
| └── package.json
├── web
+ └── mobile
+ | ├── src
+ | ...
+ ├── metro.config.js
+ ├── babel.config.js
+ └── package.json
...
And now the most important part.
We need to tell the metro where to find source files. If we don't do that, it'll only look inside the mobile
folder.
const path = require("path");
const projectRoot = __dirname;
// store path to workspace root, in our case, one level above /projects
const workspaceRoot = path.resolve(projectRoot, "../../");
module.exports = {
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
}),
},
// watch not only our mobile folder, but also root's folders
// this will include @common
watchFolders: [path.resolve(__dirname, workspaceRoot)],
resolver: {
nodeModulesPaths: [
// Tell metro to resolve modules in /mobile folder
// and if not found, then try to find modules in workspace
path.resolve(projectRoot, "node_modules"),
path.resolve(workspaceRoot, "node_modules"),
],
// And to make thing above work:
disableHierarchicalLookup: true,
},
};
So what happens above:
- we tell metro to look for files everywhere (meaning in root dir)
- resolve
node_modules
from workspace root as well.
Second part is needed, because at some point shared
code will use some packages, and they will be in root's node_modules
Okay, now metro sees codes from the shared
folder, how to import them into a mobile project?
4. Import files from shared
into mobile
Remember how we imported it on the web?
import { sayHello } from "shared";
It is possible, because shared
is a workspace. It's like having yet another node module.
But mobile
doesn't treat it as a workspace, we will import files directly. And to have similar experience as in web, we can use babel aliases:
yarn add -D babel-plugin-module-resolver
then
// mobile/babel.config.js
module.exports = {
presets: ["module:metro-react-native-babel-preset"],
plugins: [
[
"module-resolver",
{
root: ["./"],
alias: {
"shared": "../shared", // needed alias
},
},
],
],
};
And now we can do imports like this in mobile as well:
import { sayHello } from "shared";
At this point you'll be able to run mobile and web, and see how changes in shared
code reflect on both.
I also recommend to add this line to root package.json
, to have mobile packages installed from yarn
command:
{
"scripts": {
+ "postinstall": "cd ./projects/mobile && yarn"
}
}
Minimal working example [repo]
Do you see any downsides to this approach? Please, let me know in the comments.
Top comments (0)