•
14 January 2026
•
6 mins
This post outlines how I got pnpm workspaces to work in a monorepo containing React and React Native projects. I will explain key components in our pnpm workspaces configuration and walk through the errors I encountered trying to get this to work. A complete metro.config.js is attached to the bottom of this blog post.
node_modules/.pnpm folder (the pnpm virtual store).node_modules contains symlinks to this pnpm store.We have a shared package in packages/. We import this package using "<package-name>": "*" in package.json.
We use Turbo for running projects and installing dependencies efficiently.
Invalid hook callFix: Some packages like React and React Native must be pinned to specific versions to avoid multiple instances of them running at the same time. This part of our metro.config.js fixes this (Singleton Pinning):
const singletons = [
'react',
'react-native',
'expo',
'expo-router',
'expo-modules-core',
'expo-constants',
'@expo/metro-runtime',
];
config.resolver.extraNodeModules = singletons.reduce((acc, name) => {
acc[name] = path.resolve(projectRoot, 'node_modules', name);
return acc;
}, {});
Fix: Make sure to not use disableHierarchicalLookups: true. This will prevent pnpm from locating packages in the pnpm store. This part of our Metro config solved this problem:
config.watchFolders = [
path.resolve(monorepoRoot, 'packages'),
path.resolve(monorepoRoot, 'node_modules/.pnpm')
];
config.resolver.unstable_enableSymlinks = true;
config.resolver.unstable_enablePackageExports = true;
// Resolve from app's node_modules first, then root .pnpm for transitive deps
config.resolver.nodeModulesPaths = [
path.resolve(projectRoot, 'node_modules'),
path.resolve(monorepoRoot, 'node_modules'),
path.resolve(monorepoRoot, 'node_modules/.pnpm/node_modules'),
];
Fix: Install the packages with npx expo install. This will make sure compatible versions with your Expo version are used.
Getting pnpm workspaces to work with Expo requires careful Metro configuration. The key points are:
npx expo install - Always install Expo-related packages through the Expo CLI to ensure version compatibility.const path = require('path');
const fs = require('fs');
const { getDefaultConfig } = require('expo/metro-config');
const { withNativeWind } = require('nativewind/metro');
const { wrapWithReanimatedMetroConfig } = require('react-native-reanimated/metro-config');
const projectRoot = __dirname;
const monorepoRoot = path.resolve(projectRoot, '../..');
// Set EXPO_ROUTER_APP_ROOT to absolute path BEFORE config is created
// This ensures require.context resolves correctly with pnpm symlinks
const appRoot = path.resolve(projectRoot, 'app');
process.env.EXPO_ROUTER_APP_ROOT = appRoot;
process.env.EXPO_ROUTER_IMPORT_MODE = 'sync';
const config = getDefaultConfig(projectRoot);
config.projectRoot = projectRoot;
config.watchFolders = [path.resolve(monorepoRoot, 'packages'), path.resolve(monorepoRoot, 'node_modules/.pnpm')];
config.resolver.unstable_enableSymlinks = true;
config.resolver.unstable_enablePackageExports = true;
// Resolve from app's node_modules first, then root .pnpm for transitive deps
config.resolver.nodeModulesPaths = [
path.resolve(projectRoot, 'node_modules'),
path.resolve(monorepoRoot, 'node_modules'),
path.resolve(monorepoRoot, 'node_modules/.pnpm/node_modules'),
];
// Pin singletons and expo packages to prevent duplicate instances and ensure resolution
const singletons = [
'react',
'react-native',
'expo',
'expo-router',
'expo-modules-core',
'expo-constants',
'@expo/metro-runtime',
];
config.resolver.extraNodeModules = singletons.reduce((acc, name) => {
acc[name] = path.resolve(projectRoot, 'node_modules', name);
return acc;
}, {});
// Add SVG support
config.transformer.babelTransformerPath = require.resolve('react-native-svg-transformer');
config.resolver.assetExts = config.resolver.assetExts.filter(ext => ext !== 'svg');
config.resolver.sourceExts = [...config.resolver.sourceExts, 'svg'];
// Wrap with NativeWind, then Reanimated
module.exports = wrapWithReanimatedMetroConfig(withNativeWind(config, { input: './global.css' }));
Like it? Share it!