Β· 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.

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.

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.json

What Can Be Shared

Almost everything except platform-specific UI:

  1. Business Logic - API calls, data transformations, validation
  2. State Management - Zustand, Redux, or TanStack Query stores
  3. Custom Hooks - useAuth, useApi, useDebounce
  4. TypeScript Types - Shared interfaces and type definitions
  5. Utilities - Date formatting, string manipulation, calculations
  6. 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.ts

The 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

  1. Unlocks the existing React ecosystem - Millions of React components built for the web become candidates for multi-platform use
  2. Meets developers where they are - Almost every React developer knows HTML and CSS
  3. Enables practical migration - Babel codemods can transform existing codebases to use React Strict DOM
  4. 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:

  1. Monorepo structure for shared business logic, hooks, and utilities
  2. Platform-specific files for cases where you need distinct implementations
  3. 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

  1. Set up your monorepo with Turborepo, Nx, or Yarn/npm workspaces
  2. Extract shared logic into a packages/core directory
  3. Evaluate React Strict DOM for new UI components
  4. 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:

Back to Blog

Related Posts

View All Posts Β»