Using pnpm with React Native

June 22 2022

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.

Initialise a new React Native application

sh
>
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.

9445868

Delete stuff

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:

sh
>
git init

Then create an initial commit so that we don't delete everything in the next step:

sh
>
git add . && git commit -m 'initial commit'

Now we can remove some directories that we don't want anymore:

sh
>
git clean -xfd

This removes the following directories:

Removing .idea/
Removing android/.gradle/
Removing android/.idea/
Removing android/app/build/
Removing android/local.properties
Removing 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:

sh
>
rm yarn.lock

Now we can reinstall all of our dependencies with pnpm:

sh
>
pnpm install

After installing, we have our node_modules directory back (it looks a bit different now) and a pnpm-lock.yaml file.

1d5d279

Build iOS

Before we can build, we need to install our Pod dependencies:

sh
>
pnpx pod-install

Unfortunately this won't work. We'll see an error message which includes the following:

sh
>
[!] 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:

sh
>
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:

sh
>
pnpx pod-install
sh
>
pnpx react-native run-ios

4586018

Metro bundler and @rnx-kit

Once the app builds and our metro bundler starts, we run into more errors. This time, in our simulator:

sh
>
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:

sh
>
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:

jsx
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:

sh
>
pnpx react-native start

b2b58b2

Build Android

For Android, we need to add a couple of additional dependencies and we are good to go:

sh
>
pnpm install react-native-gradle-plugin jsc-android

3bf16ff

Finished

That's it. We now have both iOS and Android apps running after swtching to pnpm as our dependency manager.