React Native Cross-Platform Development: Best Practices for Production Apps
Build high-performance cross-platform mobile apps with React Native using proven patterns, native module integration, and performance optimization techniques.
React Native Cross-Platform Development: Best Practices for Production Apps
React Native has matured into a powerful framework for building production-grade mobile applications that share code between iOS and Android. Having built cross-platform apps that serve thousands of users, I've refined a set of patterns that balance code sharing with platform-specific polish. This guide covers the architecture, performance, and tooling strategies that make React Native apps feel truly native.
Why React Native in 2026?
The React Native ecosystem has evolved significantly:
- New Architecture (Fabric + TurboModules) is now stable and default
- Hermes engine delivers near-native JavaScript performance
- Expo provides a managed workflow that handles 90% of use cases
- Code sharing with React web apps through shared hooks and business logic
- TypeScript is a first-class citizen with excellent type inference
Project Architecture
Monorepo Structure with Shared Code
The most effective pattern for cross-platform development is a monorepo that separates platform-specific code from shared business logic:
apps/
├── mobile/ # React Native app
│ ├── src/
│ │ ├── screens/
│ │ ├── navigation/
│ │ └── platform/ # Native-specific components
│ └── app.json
├── web/ # Next.js web app
│ ├── src/
│ │ ├── pages/
│ │ └── components/
│ └── next.config.ts
packages/
├── shared/ # Shared business logic
│ ├── hooks/
│ ├── services/
│ ├── types/
│ └── utils/
├── ui/ # Shared UI primitives
│ ├── Button/
│ ├── Input/
│ └── Card/
└── config/ # Shared configs (ESLint, TypeScript)
This architecture enables 60-70% code reuse between web and mobile while keeping platform-specific optimizations separate.
Navigation Architecture
React Navigation v7 provides a type-safe, performant navigation system:
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
// Type-safe route params
type RootStackParamList = {
Home: undefined;
Profile: { userId: string };
Settings: undefined;
ProjectDetail: { projectId: string; title: string };
};
const Stack = createNativeStackNavigator<RootStackParamList>();
const Tab = createBottomTabNavigator();
function MainTabs() {
return (
<Tab.Navigator
screenOptions={{
tabBarStyle: { backgroundColor: '#0a0a0a' },
tabBarActiveTintColor: '#22d3ee',
headerShown: false,
}}
>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Projects" component={ProjectsScreen} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>
);
}
function AppNavigator() {
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Main" component={MainTabs} />
<Stack.Screen
name="ProjectDetail"
component={ProjectDetailScreen}
options={{ animation: 'slide_from_right' }}
/>
</Stack.Navigator>
);
}
Performance Optimization
1. List Rendering with FlashList
Replace FlatList with Shopify's FlashList for significantly better scroll performance:
import { FlashList } from '@shopify/flash-list';
interface Project {
id: string;
title: string;
description: string;
thumbnail: string;
}
function ProjectList({ projects }: { projects: Project[] }) {
const renderItem = useCallback(({ item }: { item: Project }) => (
<ProjectCard project={item} />
), []);
return (
<FlashList
data={projects}
renderItem={renderItem}
estimatedItemSize={120}
keyExtractor={(item) => item.id}
ItemSeparatorComponent={() => <View style={{ height: 12 }} />}
/>
);
}
FlashList recycles views internally and avoids the blank cell problem that plagues FlatList with large datasets.
2. Image Optimization
Use expo-image for automatic caching, blurhash placeholders, and format negotiation:
import { Image } from 'expo-image';
function ProjectThumbnail({ uri, blurhash }: { uri: string; blurhash: string }) {
return (
<Image
source={{ uri }}
placeholder={{ blurhash }}
contentFit="cover"
transition={200}
style={{ width: '100%', height: 200, borderRadius: 12 }}
recyclingKey={uri}
/>
);
}
3. Memoization Strategy
Be deliberate about memoization — don't wrap everything in useMemo:
// DO: Memoize expensive computations
const filteredProjects = useMemo(
() => projects.filter(p => p.category === selectedCategory)
.sort((a, b) => b.date.localeCompare(a.date)),
[projects, selectedCategory]
);
// DO: Memoize callbacks passed to optimized lists
const handleProjectPress = useCallback((id: string) => {
navigation.navigate('ProjectDetail', { projectId: id });
}, [navigation]);
// DON'T: Memoize simple values or inline styles
// const title = useMemo(() => "My Projects", []); // Unnecessary
Platform-Specific Patterns
Adaptive Components
Use Platform.select for platform-specific styling and behavior:
import { Platform, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
shadow: Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
},
android: {
elevation: 4,
},
}),
container: {
paddingTop: Platform.OS === 'ios' ? 44 : 0,
},
});
For more complex differences, use platform-specific file extensions:
components/
├── Header/
│ ├── index.tsx # Shared logic
│ ├── Header.ios.tsx # iOS-specific rendering
│ └── Header.android.tsx # Android-specific rendering
State Management for Mobile
Zustand + MMKV for Persistent State
Combine Zustand for app state with MMKV for fast persistent storage:
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { MMKV } from 'react-native-mmkv';
const storage = new MMKV();
const mmkvStorage = {
getItem: (name: string) => storage.getString(name) ?? null,
setItem: (name: string, value: string) => storage.set(name, value),
removeItem: (name: string) => storage.delete(name),
};
interface AppState {
theme: 'light' | 'dark';
favorites: string[];
toggleTheme: () => void;
addFavorite: (id: string) => void;
}
export const useAppStore = create<AppState>()(
persist(
(set) => ({
theme: 'dark',
favorites: [],
toggleTheme: () => set((s) => ({
theme: s.theme === 'dark' ? 'light' : 'dark',
})),
addFavorite: (id) => set((s) => ({
favorites: [...s.favorites, id],
})),
}),
{
name: 'app-storage',
storage: createJSONStorage(() => mmkvStorage),
}
)
);
MMKV is 30x faster than AsyncStorage for read/write operations — critical for apps that persist frequently.
Testing Strategy
Component Testing with React Native Testing Library
import { render, fireEvent, screen } from '@testing-library/react-native';
import { ProjectCard } from '../ProjectCard';
const mockProject = {
id: '1',
title: 'Portfolio App',
description: 'A cross-platform portfolio',
thumbnail: 'https://example.com/thumb.jpg',
};
describe('ProjectCard', () => {
it('renders project title and description', () => {
render(<ProjectCard project={mockProject} />);
expect(screen.getByText('Portfolio App')).toBeTruthy();
expect(screen.getByText('A cross-platform portfolio')).toBeTruthy();
});
it('calls onPress with project id', () => {
const onPress = jest.fn();
render(<ProjectCard project={mockProject} onPress={onPress} />);
fireEvent.press(screen.getByText('Portfolio App'));
expect(onPress).toHaveBeenCalledWith('1');
});
});
Over-the-Air Updates with EAS
Expo Application Services enables instant updates without app store review:
# Install EAS CLI
npm install -g eas-cli
# Configure update channel
eas update:configure
# Push a JS-only update
eas update --branch production --message "Fix navigation bug"
Use update channels strategically:
- production: Stable releases only
- staging: QA testing before production
- preview: Internal testing for new features
Key Takeaways
- Structure for sharing — Monorepo with shared packages maximizes code reuse
- Optimize lists — FlashList and proper memoization prevent scroll jank
- Embrace platform differences — Use platform-specific files for native polish
- MMKV over AsyncStorage — 30x faster persistent storage
- OTA updates — EAS Update for instant bug fixes without app store delays
- Type everything — TypeScript navigation params prevent runtime crashes
React Native in 2026 delivers on the promise of write once, run anywhere — as long as you know where to embrace platform-specific patterns. The key is sharing business logic while keeping UI interactions native.