Going Universal: Brownfield React Native + Next.js → One Expo App
Key Points
- Monorepo + shared UI packages
- Expo Router for unified routing
- Platform-specific .web/.native modules
Summary
A small team consolidated a brownfield Expo (React Native) app and a Next.js web app into one universal Expo project without a big rewrite. The migration was gradual: move to a Turborepo monorepo, build a shared packages/ui layer powered by react-native primitives + react-native-web, and expose platform-specific behaviors via .native/.web modules. Start with a tiny win (one shared Button), wrap shared UI in a lightweight provider for auth/analytics/theme, adopt Expo Router for file-based routing and deep links, then port features incrementally — move a feature when you touch it. The process took months while shipping product the whole time.
Key Points
- Create a monorepo (Turborepo) layout: apps/web, apps/native, packages/ui, packages/config.
- Prove the pipeline with one shared component (Button) on web, iOS, Android before broader work.
- Implement shared UI on top of React Native primitives and use react-native-web to target DOM.
- Use platform-specific files (.web.tsx / .native.tsx) for platform integrations (icons, payments, images).
- Use Nativewind for Tailwind-like styling on native; use expo-image on native and next/image on web.
- Keep a thin temporary provider that supplies user, analytics.track, feature flags, theme — apps provide concrete implementations.
- Switch mobile to Expo Router for consistent file-based routing and deep link behavior across platforms.
- Migrate features as packages (packages/ui/features/feature-*) and import them into routes/screens; refactor only when touching code.
- Handle complex integrations (e.g., Adyen payments, 3DS) by abstracting platform-specific SDKs behind common interfaces.
- Cut over by repointing web to shared modules, aligning routing, and removing the old Next.js app when safe.
Practical checklist for engineers
- Initialize Turborepo and workspace caching.
- Create packages/ui and packages/config; add TypeScript and linting configs.
- Render a single shared Button on all platforms and verify builds and types.
- Add react-native-web and platform-specific module files for divergences.
- Wrap shared UI in a temporary provider and supply platform implementations from each app.
- Adopt Expo Router for unified routing and deep links.
- Migrate features incrementally; pick one hard feature early to validate abstractions.
- Track a simple adoption metric (e.g., % screens using shared UI) to measure progress.
Notes
Expect the migration to take months, not days. The incremental approach preserves shipping velocity and keeps code duplication from growing back.