If you're not already aware, pnpm is a package manager which can be used as an alternative to npm or yarn. A full explanation of how pnpm works and its advantages over other package managers is beyond the scope of this blog but you can find more info here.
The key thing to be aware of when using pnpm is that after dependencies are installed,
your app's node_modules
folder will look quite different to how you would expect
it to look if you had installed dependencies with npm
or yarn
. pnpm
relies
heavily on symlinks and its these symlinks that cause issues for React Native projects.
By default, React Native uses Metro to bundle your JavaScript code but Metro is unable to resolve symlinks. It doesn't look like the Metro team are actively working on addressing this so we will have to look elsewhere to find a solution.
In the blog, we'll go through the process step-by-step of initialising a new React Native application and then switch from yarn
to pnpm
, addressing issues as we go. You can check out the example repo here and I'll link to the relevant commit for each step.
> npx react-native init Examplepnpm --template react-native-template-typescript
First we create a new React Native project. I'm using a TypeScript template but this is just personal preference and can be ommitted when initialising the project.
Now we have the project setup we can start the process of switching pnpm
.
As our dependencies were initally installed using yarn
, lets remove them and start fresh. You can do this however you like but I'm going to create a new git repo and then clean our the folders that I want to get rid of:
> git init
Then create an initial commit so that we don't delete everything in the next step:
> git add . && git commit -m 'initial commit'
Now we can remove some directories that we don't want anymore:
> git clean -xfd
This removes the following directories:
Removing .idea/Removing android/.gradle/Removing android/.idea/Removing android/app/build/Removing android/local.propertiesRemoving ios/Pods/Removing ios/build/Removing node_modules/
Its possible to do this without the help of git but I'm lazy and didn't want to do it manually. They should all be ignored by git, anyway.
Sadly, we do still need to manaully remove one more file:
> rm yarn.lock
Now we can reinstall all of our dependencies with pnpm
:
> pnpm install
After installing, we have our node_modules
directory back (it looks a bit different now) and a pnpm-lock.yaml
file.
Before we can build, we need to install our Pod dependencies:
> pnpx pod-install
Unfortunately this won't work. We'll see an error message which includes the following:
> [!] Invalid `Podfile` file: cannot load such file -- /Users/xxx/Code/projects/Examplepnpm/node_modules/@react-native-community/cli-platform-ios/native_modules.
We are unable to find the native_modules
script that lives in @react-native-community/cli-platform-ios
. If we look in our node_modules
directory, we see that @react-native-community/cli-platform-ios
is not listed. When installing dependenices with yarn
or npm
we get a flat node_modules
directory where all of the projects dependencies and their sub-dependencies (and so on) are listed next to each other so the path in our Podfile would typically work. But pnpm
does things differently so we only see direct dependencies listed in the node_modules
directory. And even then, they're just symlinks that point to their actual location somewhere inside node_modules/.pnpm/
.
In this case, @react-native-community/cli-platform-ios
is a dependency of react-native
and that's why our Podfile is unable to locate it in our node_modules
directory.
To solve this issue, we can add @react-native-community/cli-platform-ios
as a direct dependecy to our app. pnpm
optimises disk space by saving dependenices in a content-addressable store. We know that @react-native-community/cli-platform-ios
is already saved so we don't lose any additional disk space by doing this. Rather than creating another copy of this depenecey, pnpm
will just create a link to the copy already saved in the content-addressable store.
It seems likely that we could face a similar issue when we build our Android app so we can also install the @react-native-community/cli-platform-android
library at the same time:
> pnpm install @react-native-community/cli-platform-ios @react-native-community/cli-platform-android
Now we can re-run our pod install command and build the app for iOS:
> pnpx pod-install
> pnpx react-native run-ios
Once the app builds and our metro bundler starts, we run into more errors. This time, in our simulator:
> Error: unable to resolve module ./index from ...
As we mentioned earlier, Metro and symlinks don't really get along. Fortunately, thanks to @rnx-kit created my Microsoft, there are a couple of packages that we can add that take care of this issue:
> pnpm install @rnx-kit/metro-config @rnx-kit/metro-resolver-symlinks
And then we just need to update our metro.config.js
file to look like this:
const { makeMetroConfig } = require('@rnx-kit/metro-config');const MetroSymlinksResolver = require('@rnx-kit/metro-resolver-symlinks');
module.exports = makeMetroConfig({ projectRoot: __dirname, resolver: { resolveRequest: MetroSymlinksResolver(), },});
Now we can restart Metro using the custom symlink resolver and everything should be working:
> pnpx react-native start
For Android, we need to add a couple of additional dependencies and we are good to go:
> pnpm install react-native-gradle-plugin jsc-android
That's it. We now have both iOS and Android apps running after swtching to pnpm
as our dependency manager.