# Sharing Code Between React Native and React Web: What Works and What Does Not

> Practical guide to sharing business logic, state, and types between a React Native mobile app and a React web app — patterns that actually hold up in production.

- **Published:** 2025-05-10
- **Author:** Clixo
- **Reading time:** 6 min read
- **Tags:** react-native, code-sharing, monorepo, typescript, cross-platform
- **Canonical URL:** https://clixo.sh/blog/react-native-shared-code-web-mobile

If your product has both a mobile app and a web frontend, you have a recurring cost: logic written once in one codebase gets written again (or worse, copied) in the other. Teams that solve this well have a real engineering advantage. Teams that try to share too much end up with an unmaintainable abstraction.

Here is what is worth sharing, what is not, and how to set up the infrastructure to make sharing practical.

## What Can Be Shared Between React Native and React Web

The boundary is clear once you think about it: anything that does not depend on a rendering environment or a platform-specific API can be shared.

**Safe to share:**
- TypeScript types and interfaces (API response shapes, domain model types, form schemas)
- Business logic functions (calculations, validators, formatters, transformers)
- State management stores (Zustand stores work in both environments with no modification)
- API client code (fetch-based clients, TanStack Query query functions)
- Constants, configuration, and feature flags
- Test utilities and mock data factories

**Not safe to share:**
- Components that use React Native primitives (`View`, `Text`, `TouchableOpacity`) — these do not exist in the browser
- Components that use browser DOM APIs (`document`, `window`, `localStorage`) — these do not exist on mobile
- Navigation logic — React Navigation and browser routing (Next.js, React Router) are different systems
- Platform-specific hooks (`useWindowDimensions` in React Native vs `window.innerWidth` in web)

The most common mistake is trying to share components. Unless you are using React Native Web (which renders React Native primitives to the DOM), component sharing requires platform-specific file variants.

## Monorepo Structure for Shared Code

A monorepo with separate packages is the standard setup for real code sharing. The structure that works in practice:

```
packages/
  core/          # shared business logic, types, API clients
  ui-mobile/     # React Native components
  ui-web/        # React (web) components
apps/
  mobile/        # Expo / React Native app
  web/           # Next.js app
```

```mermaid
flowchart TD
  CORE["packages/core (types, logic, API clients, stores)"] --> MOB[apps/mobile]
  CORE --> WEB[apps/web]
  PMOB["packages/ui-mobile (React Native components)"] --> MOB
  PWEB["packages/ui-web (React web components)"] --> WEB
  MOB --> IOS[iOS App]
  MOB --> AND[Android App]
  WEB --> BROWSER[Web Browser]
```

Use Turborepo or a simple npm workspaces setup. Turborepo adds task caching which meaningfully speeds up CI builds in a monorepo. The `core` package is the one that actually gets shared.

Each app imports from `@myapp/core` for types, business logic, and API clients. Component packages are app-specific and do not cross the platform boundary.

## Setting Up the Shared Package

In `packages/core/package.json`:

```json
{
  "name": "@myapp/core",
  "main": "./src/index.ts",
  "types": "./src/index.ts"
}
```

Configure your monorepo's TypeScript project references so that both the mobile and web apps have access to the shared package's types without a build step during development:

```json
{
  "references": [{ "path": "../../packages/core" }]
}
```

For Metro (React Native's bundler), configure `watchFolders` in `metro.config.js` to include the packages directory so Metro watches shared code changes live during development.

## Sharing State with Zustand

Zustand stores that contain no platform-specific code work identically in React Native and the browser. A store for user preferences, auth state, or cart state can live in the `core` package and be imported by both apps:

```ts
// packages/core/src/stores/authStore.ts
import { create } from 'zustand';

interface AuthState {
  userId: string | null;
  token: string | null;
  login: (userId: string, token: string) => void;
  logout: () => void;
}

export const useAuthStore = create<AuthState>((set) => ({
  userId: null,
  token: null,
  login: (userId, token) => set({ userId, token }),
  logout: () => set({ userId: null, token: null }),
}));
```

This exact store can be imported and used in a React Native component and a Next.js page with identical behavior.

## Sharing API Query Functions with TanStack Query

TanStack Query (React Query) works in both React Native and React web. Define your query functions in the shared package:

```ts
// packages/core/src/queries/products.ts
export const fetchProducts = async (): Promise<Product[]> => {
  const res = await fetch('/api/products');
  if (!res.ok) throw new Error('Failed to fetch products');
  return res.json();
};

export const productsQuery = {
  queryKey: ['products'],
  queryFn: fetchProducts,
};
```

Both the mobile app and the web app import `productsQuery` and pass it to `useQuery`. Cache keys are consistent. The API client code is maintained in one place.

## Platform-Specific File Variants

For cases where you need a shared concept but different platform implementations — such as a `useStorage` hook that uses `AsyncStorage` on mobile and `localStorage` on web — use Metro and webpack's platform extension resolution:

- `useStorage.native.ts` — used by Metro (React Native)
- `useStorage.web.ts` — used by webpack or Vite (web)
- `useStorage.ts` — the TypeScript type interface, imported by both

Metro and most web bundlers resolve `.native.ts` and `.web.ts` extensions automatically when the platform-specific file exists. This lets you maintain a single import path in your components while providing separate platform implementations.

## What React Native Web Offers (and Where It Falls Short)

React Native Web renders React Native components as DOM elements. A `View` becomes a `div`, a `Text` becomes a `span`. This enables genuine component sharing across platforms.

It is a solid choice for specific use cases: design system libraries where you want the same component to work everywhere, productivity tools where visual parity across platforms matters.

It is a poor choice if your web product needs to feel like a native web application — smooth web typography, CSS Grid layouts, standard HTML form semantics. React Native Web imposes React Native's constraints (no CSS, StyleSheet only, limited web primitives) onto your web product, which often produces a web experience that feels off compared to what users expect from a browser application.

Use React Native Web deliberately, not as a default.

## The Practical Summary

For most products, the right sharing boundary is:

- **Share**: types, business logic, API clients, stores
- **Do not share**: components, navigation, platform APIs

A well-structured monorepo with a `core` package makes this clean and maintainable. The shared code is the most valuable part — types that prevent API drift, business logic that only lives in one place, query functions that keep cache keys consistent.

If your product has both mobile and web surfaces and you want the architecture set up correctly from the start, [Clixo builds full-stack products](https://clixo.sh/#contact) across web and mobile and has direct experience making this work at scale.

---

Clixo · 1141 W Bryn Mawr Ave, Itasca, IL 60143, US · [hello@clixo.sh](mailto:hello@clixo.sh)
[Start a build](https://clixo.sh/#contact) · [All services](https://clixo.sh/services) · [Agent guide (llms.txt)](https://clixo.sh/llms.txt)
