How to Convert React to React Native: Challenges and Strategies for Cross-Platform Migration

Converting a React web application to React Native requires replacing the DOM renderer with the native view renderer, abstracting platform-specific components behind unified interfaces, and adapting styling, navigation, and API access to the mobile runtime environment.

Converting React to React Native is a common requirement when teams want to leverage existing web codebase for iOS and Android deployment. While both frameworks share the same reconciler and component model as implemented in the Facebook React repository, the shift from browser-based DOM manipulation to native view rendering introduces significant architectural differences that require systematic refactoring.

Primary Challenges When Converting React to React Native

Rendering Target Differences

The fundamental divergence starts at the reconciler level. Web applications use the DOM renderer (react-dom) while React Native uses a native-view renderer (react-native). Both plug into the same reconciler algorithm defined in packages/react-reconciler/src/ReactFiberReconciler.js, but they target completely different host environments. The web renderer manipulates DOM nodes, whereas the native renderer communicates with platform-specific view managers through the bridge.

Component Primitive Mismatch

Web components like <div>, <span>, and <input> map directly to DOM elements. React Native provides its own primitive set including <View>, <Text>, <Image>, and <ScrollView> defined in the native renderer's component registry. The props accepted by each primitive differ significantly—native components use style objects rather than class names, and event handling uses synthetic touch events rather than mouse events.

Styling Model Incompatibility

The web relies on CSS with cascading style sheets, media queries, and global selectors. React Native uses a JavaScript-only style object system implemented in packages/react-native/Libraries/StyleSheet/StyleSheet.js that mimics Flexbox but lacks many CSS features such as float, grid, or advanced selectors. Styles are scoped to components by default and must be defined as objects or arrays of objects.

Web applications use URL-based routing libraries like react-router that manipulate the browser history API. Mobile applications require stack-based navigation with native gesture handling and screen transitions. The navigation state in React Native is not URL-driven but managed through native navigation controllers, requiring a fundamental shift in how you structure application flow.

Platform API Access

Web code frequently accesses browser globals like window, document, localStorage, or navigator. React Native runs in a JavaScriptCore or Hermes environment without these globals, as evidenced by the polyfill implementations in packages/react-native/Libraries/Network/fetch.js. Direct DOM manipulation or browser-specific APIs will throw runtime errors in the native environment.

Asset Handling Differences

Web bundlers serve static assets from URL paths and support dynamic imports. React Native bundles assets via the native packager and requires static require('./img.png') statements to resolve at build time. Dynamic asset loading requires platform-specific native module implementations.

Third-Party Library Compatibility

Many popular React UI libraries such as Material-UI or Ant Design are DOM-centric and depend on browser-specific APIs or CSS-in-JS implementations that target the DOM. These libraries will not function in React Native without complete reimplementation using native primitives.

Abstract UI Components Behind Platform-Agnostic Interfaces

Create a thin wrapper library that re-exports primitives (Box, Label, Button) and internally chooses the appropriate native component. Use platform-specific file extensions (Component.ios.js, Component.android.js, Component.web.js) or conditional imports based on Platform.OS to isolate platform differences.

Migrate Styling to StyleSheet Objects

Move all styles into StyleSheet.create objects and replace CSS features with Flexbox equivalents. For complex layout requirements, consider using react-native-web to share style objects between platforms, then adjust platform-specific quirks through conditional styling.

Implement a Navigation Façade

Adopt a mobile-first navigation library such as @react-navigation/native and isolate routing logic behind a small façade that can switch between react-router (web) and react-navigation (native). This allows the rest of your application to remain agnostic of the underlying navigation implementation.

Guard Browser-Only APIs

Abstract all browser-only APIs into a service layer with platform-specific implementations (e.g., StorageService.web.ts vs. StorageService.native.ts). Guard direct access to globals like window or document with if (typeof window !== 'undefined') checks to prevent runtime errors in the native environment.

Centralize Asset Management

Create a centralized asset module (assets.ts) that conditionally uses require for native builds and URL imports for web builds. This ensures consistent asset referencing across platforms while respecting the packaging requirements of each environment.

Optimize for Native Performance

Keep UI as declarative as possible and minimize expensive bridge calls by using useNativeDriver for animations and React.memo/useCallback to prevent unnecessary re-renders. Native rendering avoids browser layout engine overhead but introduces bridge communication costs that must be managed.

Practical Migration Workflow

  1. Audit the existing codebase – Identify DOM-specific components, CSS files, and browser-only APIs using static analysis (grep "window\.", grep ".css").

  2. Introduce a platform-abstraction layer – Create a src/ui/ folder with wrappers for primitives, navigation, and storage.

  3. Port styling – Convert .css/.scss files to StyleSheet.create objects; keep a shared style module for logic that can be reused on web via react-native-web.

  4. Swap renderers – Replace the entry point (ReactDOM.render) with AppRegistry.registerComponent for native builds. See React's DOM entry point: packages/react-dom/src/client/ReactDOMRoot.js.

  5. Add platform-specific implementations – Use conditional file extensions (.ios.js, .android.js, .web.js) for components that diverge.

  6. Validate with end-to-end tests – Run the existing unit tests (they should still pass) and add native-specific integration tests.

Implementation Examples

Below are minimal snippets that demonstrate the key patterns for converting React to React Native.

// src/ui/Box.tsx – Platform-agnostic primitive
import { Platform, View, Text } from 'react-native';
import { View as WebView } from 'react-native-web';

export const Box = Platform.select({
  ios: View,
  android: View,
  web: WebView,
}) as typeof View;
// src/components/Button.tsx – Wrapper that switches implementations
import { TouchableOpacity, Text, Platform } from 'react-native';
import { Button as WebButton } from '@material-ui/core';
import { Box } from '../ui/Box';

export const Button = ({ title, onPress }: { title: string; onPress: () => void }) => {
  if (Platform.OS === 'web') {
    return <WebButton onClick={onPress}>{title}</WebButton>;
  }
  return (
    <TouchableOpacity onPress={onPress}>
      <Box style={{ padding: 12, backgroundColor: '#007AFF' }}>
        <Text style={{ color: '#fff' }}>{title}</Text>
      </Box>
    </TouchableOpacity>
  );
};
// src/navigation/AppNavigator.tsx – Unified routing façade
import { Platform } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import HomeScreen from '../screens/HomeScreen';
import DetailsScreen from '../screens/DetailsScreen';

export const AppNavigator = () => {
  if (Platform.OS === 'web') {
    return (
      <BrowserRouter>
        <Switch>
          <Route exact path="/" component={HomeScreen} />
          <Route path="/details" component={DetailsScreen} />
        </Switch>
      </BrowserRouter>
    );
  }

  const Stack = createStackNavigator();
  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName="Home">
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="Details" component={DetailsScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
};
// src/services/StorageService.ts – Platform-specific storage
import { Platform } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';

export const getItem = async (key: string): Promise<string | null> => {
  if (Platform.OS === 'web') {
    return Promise.resolve(localStorage.getItem(key));
  }
  return AsyncStorage.getItem(key);
};

export const setItem = async (key: string, value: string): Promise<void> => {
  if (Platform.OS === 'web') {
    localStorage.setItem(key, value);
    return Promise.resolve();
  }
  return AsyncStorage.setItem(key, value);
};

Key Source Files in the React Repository

Understanding the underlying reconciler architecture helps clarify why converting React to React Native requires specific architectural changes. These files from the Facebook React repository illustrate how the shared reconciler plugs into different host environments.

File Why It Matters for Migration
packages/react-reconciler/src/ReactFiberReconciler.js Shows the core algorithm that both react-dom and react-native plug into; understanding it clarifies why the host config must differ.
packages/react-dom/src/client/ReactDOMRoot.js Entry point for the web renderer – useful when swapping to AppRegistry.registerComponent for native.
packages/react-native/ReactCommon/react/renderer/flight/RendererHostConfig.js Host configuration for the native renderer; demonstrates how native views are created and updated.
packages/react-native/Libraries/Components/View/ViewProps.android.js Definition of native view props – helps map DOM attributes to native equivalents.
packages/react-native/Libraries/StyleSheet/StyleSheet.js Core styling implementation that differs from CSS; essential when porting .css files.
packages/react-native/Libraries/Network/fetch.js Polyfill for fetch in the native environment; illustrates how browser-based networking is replicated.

These files give you a concrete view of how React’s core reconciler is leveraged by two distinct renderers, and they serve as reference points when you need to adapt web-specific code to the native runtime.

Summary

  • Rendering architecture differs fundamentally: React uses react-dom to manipulate the browser DOM, while React Native uses a native view renderer through react-reconciler, requiring you to replace HTML primitives with native components.

  • Abstract platform differences: Create wrapper components and service layers that use Platform.select or platform-specific file extensions (.ios.js, .android.js, .web.js) to isolate web and native implementations.

  • Migrate styling systems: Convert CSS files to StyleSheet.create objects, replace CSS Grid and float layouts with Flexbox, and use react-native-web to share styles between platforms where possible.

  • Adapt navigation patterns: Replace URL-based routing with stack-based navigation using @react-navigation/native, or create a façade that switches between react-router for web and React Navigation for native.

  • Guard browser-only APIs: Abstract access to window, document, and localStorage into platform-specific services that use AsyncStorage on native and web APIs on browsers.

Frequently Asked Questions

How long does it typically take to convert React to React Native?

The timeline depends on the application's complexity and the depth of browser-specific dependencies. A simple component-based app with minimal DOM manipulation might take a few weeks, while complex applications using extensive CSS frameworks, canvas rendering, or browser-specific APIs could require several months of refactoring to implement proper platform abstractions.

Can I share code between React web and React Native applications?

Yes, significant portions of business logic, custom hooks, state management, and utility functions can be shared between platforms. The key is to isolate platform-specific concerns—UI components, styling, navigation, and storage—behind abstraction layers while keeping pure JavaScript logic in shared modules that import platform-agnostic dependencies only.

What is the most common mistake when converting React to React Native?

The most frequent error is attempting to use DOM-specific methods or browser globals directly in React Native components, such as calling document.querySelector or window.addEventListener without platform guards. Since React Native runs in a JavaScriptCore environment without these globals, such calls immediately crash the application, requiring developers to abstract all platform API access through conditional checks or service layers.

Should I use react-native-web to simplify the conversion process?

Using react-native-web can be an effective strategy if you plan to maintain both web and mobile platforms long-term, as it allows you to write React Native components that render to the DOM. However, if you are permanently migrating away from the web, introducing react-native-web adds unnecessary overhead; instead, directly port components to native primitives and use platform-specific file extensions to handle any remaining web compatibility needs during the transition.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →