Β· Mobile Development Β· 7 min read
Sharing Code Between React and React Native - Monorepos and React Strict DOM
Learn practical techniques for maximizing code reuse between web and native applications, from monorepo architecture to the revolutionary React Strict DOM approach.
Building applications for both web and mobile has traditionally meant maintaining two separate codebases with duplicated logic, inconsistent user experiences, and double the maintenance burden. But it doesnβt have to be this way. In this article, weβll explore proven techniques for sharing code between React and React Native, from battle-tested monorepo structures to the game-changing React Strict DOM.
The Cost of Fragmentation
The first decision a developer makesββWeb or Native?ββoften locks them into a specific set of APIs and paradigms. As Nicolas Gallagher points out:
This bifurcation leads to hidden engineering inefficiencies and the need to make tough trade-offs. Doing similar UI work twice correlates with slower time-to-market and greater product inconsistency.
This fragmentation affects not just development speed, but also AI-assisted coding. When an LLM must learn several different ways to achieve similar results with React, its output becomes slower, less reliable, and more expensive.
Strategy 1: The Monorepo Approach
A monorepo is the foundation for any serious code-sharing strategy. By housing your web and native applications in a single repository, you create the infrastructure needed for shared packages.
Recommended Structure
my-app/
βββ apps/
β βββ web/ # React web application
β β βββ package.json
β β βββ src/
β βββ mobile/ # React Native application
β β βββ package.json
β β βββ src/
β βββ desktop/ # Electron or Tauri app (optional)
β βββ ...
βββ packages/
β βββ core/ # Business logic, state, utilities
β β βββ package.json
β β βββ src/
β β βββ hooks/
β β βββ services/
β β βββ stores/
β β βββ utils/
β βββ ui/ # Shared UI components
β β βββ package.json
β β βββ src/
β βββ config/ # Shared configuration
β βββ ...
βββ package.json
βββ turbo.json # or nx.jsonWhat Can Be Shared
Almost everything except platform-specific UI:
- Business Logic - API calls, data transformations, validation
- State Management - Zustand, Redux, or TanStack Query stores
- Custom Hooks -
useAuth,useApi,useDebounce - TypeScript Types - Shared interfaces and type definitions
- Utilities - Date formatting, string manipulation, calculations
- Constants - API endpoints, feature flags, configuration
Example: Shared API Layer
// packages/core/src/services/userService.ts
export interface User {
id: string;
name: string;
email: string;
}
export async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error('Failed to fetch user');
return response.json();
}
export async function updateUser(id: string, data: Partial<User>): Promise<User> {
const response = await fetch(`/api/users/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) throw new Error('Failed to update user');
return response.json();
}This service works identically in both React and React Nativeβno platform-specific code needed.
Example: Shared Custom Hook
// packages/core/src/hooks/useUser.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { fetchUser, updateUser, User } from '../services/userService';
export function useUser(userId: string) {
return useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
}
export function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: Partial<User> }) =>
updateUser(id, data),
onSuccess: (data, { id }) => {
queryClient.setQueryData(['user', id], data);
},
});
}Both your web and mobile apps import and use this hook exactly the same way.
Strategy 2: Platform-Specific Files
For components that need different implementations on web vs. native, use the .native.js / .web.js extension pattern:
packages/ui/src/
βββ Button/
β βββ Button.tsx # Shared logic and types
β βββ Button.web.tsx # Web-specific rendering
β βββ Button.native.tsx # Native-specific rendering
β βββ index.tsThe bundler automatically resolves the correct file based on the target platform.
// Button.tsx - Shared props and logic
export interface ButtonProps {
label: string;
onPress: () => void;
variant?: 'primary' | 'secondary';
disabled?: boolean;
}
// Button.web.tsx
import { ButtonProps } from './Button';
export function Button({ label, onPress, variant = 'primary', disabled }: ButtonProps) {
return (
<button
onClick={onPress}
disabled={disabled}
className={`btn btn-${variant}`}
>
{label}
</button>
);
}
// Button.native.tsx
import { ButtonProps } from './Button';
import { TouchableOpacity, Text, StyleSheet } from 'react-native';
export function Button({ label, onPress, variant = 'primary', disabled }: ButtonProps) {
return (
<TouchableOpacity
onPress={onPress}
disabled={disabled}
style={[styles.button, styles[variant]]}
>
<Text style={styles.label}>{label}</Text>
</TouchableOpacity>
);
}Strategy 3: React Strict DOM - The Future of Code Sharing
While monorepos and platform-specific files work well, they still require maintaining two separate UI implementations. React Strict DOM represents a paradigm shift: write once using web APIs, run everywhere with native quality.
What is React Strict DOM?
React Strict DOM is a library that standardizes multi-platform React development on web APIs. It provides a strict compatibility layer between React DOM and React Native:
- On the web, components render to standard HTML elements and static CSS
- On native platforms, the same components render to native views
The key insight is that web APIs are the most widely understood programming interface in the world. By using them as the common standard, React Native becomes accessible to web developers without learning a new paradigm.
How It Works
React Strict DOM exports two primary modules: html and css.
import { css, html } from 'react-strict-dom';
const styles = css.create({
container: {
display: 'flex',
padding: '1rem',
backgroundColor: '#f5f5f5',
},
heading: {
fontSize: '1.5rem',
fontWeight: 'bold',
color: '#333',
},
text: {
fontSize: '1rem',
lineHeight: 1.6,
}
});
function WelcomeCard({ title, description }) {
return (
<html.div style={styles.container}>
<html.h2 style={styles.heading}>{title}</html.h2>
<html.p style={styles.text}>{description}</html.p>
</html.div>
);
}This code runs identically on web and native. No conditional logic, no platform checksβReact Strict DOM handles the translation automatically.
Why This Matters
- Unlocks the existing React ecosystem - Millions of React components built for the web become candidates for multi-platform use
- Meets developers where they are - Almost every React developer knows HTML and CSS
- Enables practical migration - Babel codemods can transform existing codebases to use React Strict DOM
- Future-proofs your investment - Web standards have proven remarkably stable over decades
Production Validation
Meta has been using React Strict DOM in production since 2023. The Facebook and Instagram VR apps, unveiled at Meta Connect 2024, demonstrate the approach at scale:
Over 60% of the files used by the Facebook VR app were shared directly with facebook.com, including some of the most sophisticated features on the web, like the news feed, commenting systems, content rendering logic, and router.
The VR apps look and feel indistinguishable from apps built natively from scratch, proving that code sharing doesnβt require compromising on native quality.
Putting It All Together
The ideal architecture combines all three strategies:
- Monorepo structure for shared business logic, hooks, and utilities
- Platform-specific files for cases where you need distinct implementations
- React Strict DOM for UI components that can be truly universal
my-app/
βββ apps/
β βββ web/
β βββ mobile/
βββ packages/
β βββ core/ # 100% shared business logic
β β βββ hooks/
β β βββ services/
β β βββ stores/
β βββ ui/ # React Strict DOM components
β β βββ Button.tsx
β β βββ Card.tsx
β β βββ Form.tsx
β βββ platform/ # Platform-specific when needed
β βββ Camera/
β β βββ Camera.web.tsx
β β βββ Camera.native.tsx
β βββ ...Getting Started
- Set up your monorepo with Turborepo, Nx, or Yarn/npm workspaces
- Extract shared logic into a
packages/coredirectory - Evaluate React Strict DOM for new UI components
- Migrate incrementally - you donβt need to rewrite everything at once
The goal isnβt 100% code sharingβitβs maximizing reuse where it makes sense while maintaining the flexibility to optimize for each platform when needed.
Conclusion
The fragmentation between React and React Native is no longer an inevitable cost of cross-platform development. With proper monorepo architecture and emerging tools like React Strict DOM, weβre moving toward a future where βwrite once, run everywhereβ is actually achievable without sacrificing quality.
As Nicolas Gallagher concludes: βWhen we constrain how we build, we can create trade-offs that work in our favor. Trading some flexibility to increase predictability is what lets us compose larger systems with more confidence.β
Start small, share what makes sense, and gradually expand your shared codebase as your team gains confidence in the approach.
Further Reading:
- One React for Web and Native by Nicolas Gallagher
- React Strict DOM Documentation
- Turborepo Documentation