monorepo strategies in practice
Realistic code sharing between React web and React Native does not mean using the same components everywhere, it means deliberately separating what is platform agnostic from what cannot be. Business logic, types, API clients and validation share cleanly, interface components mostly do not. This article shows a working monorepo structure with real examples.
Table of Contents
- 1. What can really be shared and what cannot
- 2. Monorepo base structure with pnpm workspaces
- 3. Shared business logic and custom hooks
- 4. Shared types and API clients
- 5. Platform specific files for the UI layer
- 6. Sharing styling with NativeWind
- 7. React Native Web as a special case
- 8. Pitfalls in shared code
- 9. Strategies compared directly
- 10. Summary
- 11. FAQ
1. What can really be shared and what cannot
The most common misconception when trying to share code between React web and React Native is the assumption that complete interface components can be reused unchanged. That does not work, because the two platforms use different base elements: div and span on the web versus View and Text in React Native. Anyone who tries to share this layer either produces wrapper abstractions that serve both worlds worse than native implementations, or abandons the effort frustrated after a short time.
Realistic code sharing instead targets the layers below presentation: business logic, state management, API communication, validation rules and TypeScript types. These layers have no dependency on View, div or other platform specific elements and can be shared at nearly 100 percent. In well structured projects, the shared code share is often 60 to 80 percent of the total logic, while the actual presentation layer stays platform specific.
This separation is not a limitation, it follows the same architecture that would make sense even without cross platform ambitions: a clear separation between domain logic and presentation. Anyone whose React web project is already structured this way finds surprisingly little resistance when entering React Native, because the shareable building blocks already sit isolated.
2. Monorepo base structure with pnpm workspaces
The technical foundation for code sharing is almost always a monorepo with pnpm workspaces or Yarn workspaces, orchestrated by Turborepo for fast, cached builds. The structure separates apps/ for the standalone applications, meaning the web app and the React Native app, from packages/ for shared libraries such as business logic, types and design tokens. Every package in the packages/ folder has its own package.json and can be versioned and tested independently.
Turborepo caches build and test results per package, so a change in the React Native app does not trigger a rebuild of the unchanged web app. In growing projects, this caching mechanism is not a nicety but a necessity for keeping CI runtimes under control. The configuration in turbo.json defines the dependency graphs between tasks, so a build in apps/mobile automatically runs build in packages/shared-logic first.
# Monorepo layout for sharing code between React web and React Native
myapp/
apps/
web/ # React web app (Vite or Next.js)
mobile/ # React Native app (Expo)
packages/
shared-logic/ # Business logic, hooks, validation
shared-types/ # TypeScript types and API contracts
api-client/ # Fetch wrapper and endpoint definitions
package.json
pnpm-workspace.yaml
turbo.json
# Install and link all workspace packages
pnpm install
# Run mobile app dev server, shared packages auto-linked
pnpm --filter mobile dev
3. Shared business logic and custom hooks
Custom hooks without a DOM dependency are the most productive area for code sharing between React web and React Native. A hook that implements form validation, performs cart calculations or fetches data with TanStack Query typically contains not a single reference to document, window or a native element. Such hooks move unchanged into a shared package and get imported by both apps.
A concrete example is a cart management hook that encapsulates quantity changes, price calculation and discount logic. In an e-commerce app, this logic is identical on web and mobile, only the presentation of the results differs. By extracting it into packages/shared-logic, a bug fix or a new discount rule gets implemented in exactly one place and automatically takes effect on both platforms, without the risk of web and mobile drifting apart over time.
// packages/shared-logic/src/useCart.ts
// Platform-agnostic cart logic, shared between web and React Native
import { useState, useCallback, useMemo } from 'react';
import type { CartItem, Product } from '@myapp/shared-types';
export function useCart() {
const [items, setItems] = useState<CartItem[]>([]);
const addItem = useCallback((product: Product, quantity = 1) => {
setItems((prev) => {
const existing = prev.find((i) => i.productId === product.id);
if (existing) {
return prev.map((i) =>
i.productId === product.id
? { ...i, quantity: i.quantity + quantity }
: i
);
}
return [...prev, { productId: product.id, quantity, price: product.price }];
});
}, []);
const total = useMemo(
() => items.reduce((sum, item) => sum + item.price * item.quantity, 0),
[items]
);
return { items, addItem, total };
}
4. Shared types and API clients
TypeScript types for API responses, domain models and form schemas should always live in a shared package, regardless of how much or how little other code is shared. A shared Product type definition prevents web and React Native from making incompatible assumptions about the data structure over time, a problem that otherwise only surfaces late and hard to trace in production.
The API client itself, usually a thin wrapper around fetch with error handling and retry logic, can also be shared entirely, as long as it does not contain platform specific authentication. It is important to abstract access to secure storage, for instance AsyncStorage in React Native versus localStorage on the web, behind a common interface whose concrete implementation is swapped per platform. This keeps the API client itself platform agnostic, while only the underlying storage implementation varies.
5. Platform specific files for the UI layer
For the presentation layer, which naturally cannot be shared identically, the React Native bundler Metro offers an elegant mechanism: file extensions such as .native.tsx and .web.tsx. A file named ProductCard.native.tsx is automatically resolved only for React Native, ProductCard.web.tsx only for the web app, while both are referenced through the same import path ./ProductCard. Calling code therefore needs no branching logic at all.
This mechanism works cleanest when both variants implement the same props interface, defined in a shared type from packages/shared-types. That ensures the web and native variants of a component stay interchangeable, without callers needing to know which platform they currently run on. In Next.js projects, an additional Babel or webpack setup handles the same resolution, since Next.js does not natively know Metro's platform file convention.
// ProductCard.native.tsx - resolved automatically inside the Expo app
import { View, Text, Pressable } from 'react-native';
import type { ProductCardProps } from '@myapp/shared-types';
export function ProductCard({ product, onAddToCart }: ProductCardProps) {
return (
<Pressable onPress={() => onAddToCart(product)}>
<View style={{ padding: 16 }}>
<Text style={{ fontWeight: '600' }}>{product.name}</Text>
<Text>{product.price.toFixed(2)} EUR</Text>
</View>
</Pressable>
);
}
// ProductCard.web.tsx - resolved automatically inside the Vite/Next.js app
export function ProductCardWeb({ product, onAddToCart }: ProductCardProps) {
return (
<button onClick={() => onAddToCart(product)} className="p-4 text-left">
<p className="font-semibold">{product.name}</p>
<p>{product.price.toFixed(2)} EUR</p>
</button>
);
}
6. Sharing styling with NativeWind
NativeWind translates Tailwind CSS class names into React Native style objects at build time, making it the most practical way to maintain a consistent design system across web and mobile. Instead of maintaining two entirely separate styling approaches, CSS for web and StyleSheet objects for native, teams using NativeWind write the same utility classes in both environments, for instance className="p-4 bg-slate-900 rounded-xl".
Design tokens such as colors, spacing and typography can be defined in a shared Tailwind configuration inside packages/design-tokens and referenced by both apps. That prevents design drift between web and mobile, a problem that creeps in gradually in cross platform teams without a shared token system, because small adjustments to color values get made in one app without updating the other.
7. React Native Web as a special case
React Native Web takes the opposite approach: instead of adopting React web components into React Native, this library compiles React Native components such as View and Text into DOM elements in the browser. This approach suits teams that primarily think mobile first and want to treat the web app as an additional output platform of the same codebase, for instance in Expo projects with expo start --web.
The tradeoff: the resulting web markup is often less semantic than hand written HTML, and SEO critical pages usually benefit more from a genuine web first solution with Next.js. React Native Web is excellent for internal tools, admin dashboards and applications without SEO requirements, but is rarely the right choice for public, search optimized marketing pages.
8. Pitfalls in shared code
A common pitfall is accidentally introducing platform specific code into a package that is supposed to be shared, for instance an import of react-native-async-storage in a hook that the web app is also meant to use. Such dependencies break the web app's build immediately and are usually only visible through a failed CI run, not through local development, if only the mobile app is being worked on at the time.
A second pitfall concerns excessive ambition in sharing: trying to unify the presentation layer too with generic wrapper components regularly leads to abstractions that are optimal for neither web nor React Native. The rule of thumb is to share logic consistently and keep presentation consistently platform specific, rather than mixing the two.
9. Strategies compared directly
The following overview shows which code sharing strategy fits which use case best between React web and React Native.
| Layer | Shareability | Recommended strategy | Edge case |
|---|---|---|---|
| Business logic, hooks | Nearly 100 percent | Shared package, no platform code | Abstract storage access |
| Types and contracts | 100 percent | Shared types package | None |
| Presentation components | Low | Platform files .native/.web | Same props interface |
| Styling | Medium | NativeWind with shared tokens | Not every CSS feature available |
| Entire interface | Only for internal tools | React Native Web | Weaker for SEO requirements |
This overview shows that a blanket goal of one hundred percent shared code is unrealistic and usually not even sensible. The pragmatic approach shares consistently what is shareable and accepts platform specific presentation as a deliberate design decision, not a compromise.
Mironsoft
Monorepo architecture for web and React Native teams
Maintaining web and mobile from one codebase?
We set up your Turborepo monorepo, cleanly separate shared business logic from platform specific presentation, and configure CI pipelines for web and React Native together.
Monorepo setup
Turborepo, pnpm workspaces and shared packages from scratch
Logic extraction
Extracting existing business logic from web only code, platform agnostic
Design system
NativeWind setup with shared design tokens for web and mobile
10. Summary
Successful code sharing between React web and React Native rests on a clear layer separation: business logic, types and API clients move almost entirely into shared packages of a monorepo, while the presentation layer deliberately stays platform specific, supported by Metro's file extension convention .native.tsx and .web.tsx. Turborepo and pnpm workspaces provide the technical foundation for fast, cached builds across multiple apps.
Anyone who implements this separation consistently avoids the typical pitfalls of cross platform projects: platform specific dependencies in shared code and overambitious presentation abstractions. NativeWind and shared design tokens close the gap in styling, while React Native Web can be a sensible addition for internal tools without SEO requirements.
Sharing Code Between React Web and React Native, the key points at a glance
Monorepo
Turborepo with pnpm workspaces separates apps from shared packages and caches builds per package.
Shared logic
Custom hooks, types and API clients without a DOM dependency can be shared almost entirely.
Platform files
.native.tsx and .web.tsx automatically resolve to the right UI variant, same props interface.
Styling
NativeWind with shared design tokens prevents design drift between web and mobile.