We split a single React Native app that carried two user roles (customer and service provider) into two separate apps sharing a package. Both are live on Google Play and the App Store. RN 0.84, React 19, npm, bare workflow, no Expo.
Every monorepo guide I found stops at watchFolders. That gets you a bundle that builds. It does not get you an app that works.
The bug that cost the most time
Sign-in on a shared auth screen returned 200. A token was written to storage. The app sat on the login screen.
Nothing threw. Nothing logged. The network tab was clean.
The cause was two copies of zustand. Metro resolves a bare import from the importing file's own location, so shared/src/store/authStore.ts walked up to repo-root/node_modules/zustand, while the app code walked up to app-customer/node_modules/zustand. Two module instances, two stores. The shared auth screen wrote to one. The app shell read the other.
Both resolutions succeeded. That is exactly why there is no error to search for.
The same mechanism, four more ways
-
Two copies of React. This one at least crashes, with
Cannot read property 'current' of nullthrown from a shared component that is obviously correct. - Two copies of axios. Interceptors are instance state, so auth headers silently stop being attached on requests made from shared code.
- Native modules registering twice. AsyncStorage, geolocation, vector icons. Symptoms vary and none of them name the cause.
-
A green
tscthat proves nothing. Two copies of a module have identical types. The type checker's job is to agree that they match, and it will do that job perfectly while the app is broken.
The fix
A resolveRequest in each app's metro.config.js, pinning every dependency the shared package imports to that app's own copy. The list is short and finite, and you should derive it with grep rather than from memory.
The full writeup
I put the whole thing in a repo, including the npm side (the shared package declares zero dependencies, and why declaring peerDependencies instead gives you a permanent ERESOLVE) and the Android side (a workspace install intermittently left a partial copy of @react-native/gradle-plugin in the app's own node_modules, shadowing the complete one, and the build died at settings evaluation in about a second).
https://github.com/akavinashsingh/react-native-two-app-split
MIT, nothing to install. If any of it is wrong for your setup I would rather hear it than not.
Top comments (0)