Solving React Native Codegen & Kotlin Errors with pnpm in Monorepos: Just Hoist It!

If you’re building a React Native project within a pnpm monorepo, you might have stumbled into a frustrating maze of codegen and Kotlin compilation errors. The good news? The solution is surprisingly simple, even if the journey to find it can be a headache. The key is to embrace full hoisting with node-linker=hoisted and adjust your build.gradle paths accordingly. Forget selective hoisting – it’s a rabbit hole you don’t want to go down.

The Common Pitfall: React Native & pnpm’s Incompatible Expectations

React Native’s intricate build system, particularly around code generation (codegen) and native module linking, expects a flat node_modules structure where all dependencies are easily discoverable. pnpm, by default, uses a strict symlinking approach that creates a non-hoisted, content-addressable node_modules – a highly efficient system that often clashes with React Native’s assumptions.

This mismatch typically manifests as errors like:

  • Error: Cannot find module '.../@react-native/codegen/lib/cli/combine/combine-js-to-schema-cli.js'
  • Unresolved reference 'NativeRNWalletConnectModuleSpec'
  • Kotlin compilation failures (“overrides nothing,” “unresolved reference”)

These errors often appear during Android builds, pointing to issues with codegen finding its tools or native modules failing to locate generated classes.

Why Selective Hoisting Is a False Optimization

Your first instinct might be to selectively hoist problematic packages like @react-native/codegen or @react-native/gradle-plugin. While seemingly logical, this often leads to:

  • Inconsistent module resolution (some packages find hoisted versions, others don’t).
  • Different behaviors between development and build environments.
  • Further Kotlin compilation errors as other native modules fail.
  • A never-ending game of whack-a-mole as React Native’s internal dependency structure evolves with updates.

React Native’s dependency graph is complex and constantly changing. Trying to manually maintain a perfect hoistPattern is an uphill battle that will cost you significant debugging time.

The Real Solution: Full Hoisting and Path Adjustments

After countless hours of debugging, the most robust and future-proof solution is to simply hoist everything. This provides the flat node_modules structure that React Native expects, ensuring consistency across all its tooling.

Here’s how to implement it:

1. Enable Hoisting in .npmrc

Create a .npmrc file at the root of your monorepo (or standalone project if applicable) and add:

node-linker=hoisted

After adding this, run pnpm install again. This tells pnpm to create a traditional, hoisted node_modules directory where all dependencies are at the top level, making them discoverable for React Native’s build tools.

2. Adjust build.gradle Paths for Monorepos

If your React Native app is nested within a monorepo (e.g., monorepo-root/mobile), you’ll need to explicitly tell your Android build.gradle where to find the hoisted node_modules.

Navigate to mobile/android/app/build.gradle and modify the react block to point to the root node_modules:

react {
    // Point to root node_modules since we're using hoisted linking in a monorepo
    reactNativeDir = file("../../../node_modules/react-native")
    codegenDir = file("../../../node_modules/@react-native/codegen")
    cliFile = file("../../../node_modules/react-native/cli.js")

    autolinkLibrariesWithApp()
}

The ../../../ path navigates from mobile/android/app/ up to the monorepo root, where your hoisted node_modules now resides.

Note: If you’re using pnpm with a standalone React Native project (not a monorepo), you only need the .npmrc change. The build.gradle modifications are only necessary when your React Native app is nested.

Why This Approach Works (and Saves You Hours)

By using node-linker=hoisted, you provide React Native’s build system with the consistent, predictable dependency structure it expects. This eliminates the headaches caused by symlinking inconsistencies, ensuring that:

  • Codegen tools can find all necessary modules.
  • Kotlin compilation correctly resolves generated classes.
  • Metro bundler works seamlessly.

This approach is not only simpler to set up but also more resilient to future React Native updates, as you’re aligning with the expected environment. While selective hoisting might appeal for disk space optimization, the maintenance burden and debugging time saved by full hoisting far outweigh any minor benefits for most projects.

Conclusion

Don’t fight React Native’s expectations when using pnpm, especially in a monorepo. Embrace node-linker=hoisted and adjust your build.gradle paths to point to the root node_modules. This straightforward solution will save you countless hours of debugging and ensure a stable, reliable build process for your React Native applications.

Focus your energy on building awesome features, not untangling dependency woes!

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.
You need to agree with the terms to proceed