react-native
Expert React Native and Expo development skill for building cross-platform mobile apps. Use this skill when creating, debugging, or optimizing React Native projects - Expo setup, native modules, navigation (React Navigation, Expo Router), performance tuning (Hermes, FlatList, re-render prevention), OTA updates (EAS Update, CodePush), and bridging native iOS/Android code. Triggers on mobile app architecture, Expo config plugins, app store deployment, push notifications, and React Native CLI tasks.
engineering react-nativeexpomobileiosandroidcross-platformWhat is react-native?
Expert React Native and Expo development skill for building cross-platform mobile apps. Use this skill when creating, debugging, or optimizing React Native projects - Expo setup, native modules, navigation (React Navigation, Expo Router), performance tuning (Hermes, FlatList, re-render prevention), OTA updates (EAS Update, CodePush), and bridging native iOS/Android code. Triggers on mobile app architecture, Expo config plugins, app store deployment, push notifications, and React Native CLI tasks.
react-native
react-native is a production-ready AI agent skill for claude-code, gemini-cli, openai-codex, and 1 more. Expert React Native and Expo development skill for building cross-platform mobile apps. Use this skill when creating, debugging, or optimizing React Native projects - Expo setup, native modules, navigation (React Navigation, Expo Router), performance tuning (Hermes, FlatList, re-render prevention), OTA updates (EAS Update, CodePush), and bridging native iOS/Android code.
Quick Facts
| Field | Value |
|---|---|
| Category | engineering |
| Version | 0.1.0 |
| Platforms | claude-code, gemini-cli, openai-codex, mcp |
| License | MIT |
How to Install
- Make sure you have Node.js installed on your machine.
- Run the following command in your terminal:
npx skills add AbsolutelySkilled/AbsolutelySkilled --skill react-native- The react-native skill is now available in your AI coding agent (Claude Code, Gemini CLI, OpenAI Codex, etc.).
Overview
A comprehensive mobile development skill covering the full React Native ecosystem - from bootstrapping an Expo project to shipping production apps on iOS and Android. It encodes deep expertise in Expo (managed and bare workflows), React Navigation and Expo Router, native module integration, Hermes-powered performance optimization, and over-the-air update strategies. Whether you are building a greenfield app or maintaining a complex production codebase, this skill provides actionable patterns grounded in real-world mobile engineering.
Tags
react-native expo mobile ios android cross-platform
Platforms
- claude-code
- gemini-cli
- openai-codex
- mcp
Related Skills
Pair react-native with these complementary skills:
Frequently Asked Questions
What is react-native?
Expert React Native and Expo development skill for building cross-platform mobile apps. Use this skill when creating, debugging, or optimizing React Native projects - Expo setup, native modules, navigation (React Navigation, Expo Router), performance tuning (Hermes, FlatList, re-render prevention), OTA updates (EAS Update, CodePush), and bridging native iOS/Android code. Triggers on mobile app architecture, Expo config plugins, app store deployment, push notifications, and React Native CLI tasks.
How do I install react-native?
Run npx skills add AbsolutelySkilled/AbsolutelySkilled --skill react-native in your terminal. The skill will be immediately available in your AI coding agent.
What AI agents support react-native?
This skill works with claude-code, gemini-cli, openai-codex, mcp. Install it once and use it across any supported AI coding agent.
Maintainers
Generated from AbsolutelySkilled
SKILL.md
React Native
A comprehensive mobile development skill covering the full React Native ecosystem - from bootstrapping an Expo project to shipping production apps on iOS and Android. It encodes deep expertise in Expo (managed and bare workflows), React Navigation and Expo Router, native module integration, Hermes-powered performance optimization, and over-the-air update strategies. Whether you are building a greenfield app or maintaining a complex production codebase, this skill provides actionable patterns grounded in real-world mobile engineering.
When to use this skill
Trigger this skill when the user:
- Wants to create, configure, or scaffold a React Native or Expo project
- Needs help with React Navigation or Expo Router (stacks, tabs, deep linking)
- Is writing or debugging a native module or Turbo Module bridge
- Asks about mobile performance (Hermes, FlatList optimization, re-render prevention)
- Wants to set up OTA updates with EAS Update or CodePush
- Needs guidance on Expo config plugins or prebuild customization
- Is deploying to the App Store or Google Play (EAS Build, Fastlane, signing)
- Asks about push notifications, background tasks, or device APIs in React Native
Do NOT trigger this skill for:
- Web-only React development with no mobile component
- Flutter, Swift-only, or Kotlin-only native app development
Setup & authentication
Environment variables
EXPO_TOKEN=your-expo-access-token
# Optional: for EAS Build and Update
EAS_BUILD_PROFILE=productionInstallation
# Create a new Expo project (recommended starting point)
npx create-expo-app@latest my-app
cd my-app
# Or add Expo to an existing React Native project
npx install-expo-modules@latest
# Install EAS CLI for builds and updates
npm install -g eas-cli
eas loginBasic initialisation
// app/_layout.tsx (Expo Router - file-based routing)
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="index" options={{ title: 'Home' }} />
<Stack.Screen name="details" options={{ title: 'Details' }} />
</Stack>
);
}// app.json / app.config.ts (Expo configuration)
import { ExpoConfig } from 'expo/config';
const config: ExpoConfig = {
name: 'MyApp',
slug: 'my-app',
version: '1.0.0',
orientation: 'portrait',
icon: './assets/icon.png',
splash: { image: './assets/splash.png', resizeMode: 'contain' },
ios: { bundleIdentifier: 'com.example.myapp', supportsTablet: true },
android: { package: 'com.example.myapp', adaptiveIcon: { foregroundImage: './assets/adaptive-icon.png' } },
plugins: [],
};
export default config;Core concepts
React Native renders native platform views (UIView on iOS, Android View on Android) driven by JavaScript business logic. The architecture has evolved through three eras:
The Bridge (Legacy): JS and native communicate via an asynchronous JSON bridge. All data is serialized/deserialized. This is the bottleneck behind most performance complaints in older RN apps.
The New Architecture (Fabric + TurboModules): Released as default in RN 0.76+. Fabric replaces the old renderer with synchronous, concurrent-capable rendering. TurboModules replace the bridge with JSI (JavaScript Interface) - direct C++ bindings for native module calls with no serialization overhead. Codegen generates type-safe interfaces from TypeScript specs.
Expo as the Platform Layer: Expo provides a managed layer on top of React Native - prebuild (generates native projects from config), EAS (cloud build and OTA update services), Expo Modules API (write native modules in Swift/Kotlin with a unified API), and Expo Router (file-based navigation). The vast majority of new RN projects should start with Expo. "Bare workflow" is only needed when Expo's managed layer cannot accommodate a specific native requirement.
Navigation Model: React Navigation (imperative) and Expo Router (file-based, built on React Navigation) are the standard. Navigation state lives in a stack machine - screens push/pop onto stacks, tabs switch between stack navigators, and drawers wrap stacks. Deep linking maps URLs to screen paths.
Common tasks
1. Set up navigation with Expo Router
File-based routing where the file system defines the navigation structure.
// app/_layout.tsx - Root layout with tabs
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
export default function Layout() {
return (
<Tabs screenOptions={{ tabBarActiveTintColor: '#007AFF' }}>
<Tabs.Screen
name="index"
options={{ title: 'Home', tabBarIcon: ({ color }) => <Ionicons name="home" size={24} color={color} /> }}
/>
<Tabs.Screen
name="profile"
options={{ title: 'Profile', tabBarIcon: ({ color }) => <Ionicons name="person" size={24} color={color} /> }}
/>
</Tabs>
);
}// app/details/[id].tsx - Dynamic route with params
import { useLocalSearchParams } from 'expo-router';
import { Text, View } from 'react-native';
export default function Details() {
const { id } = useLocalSearchParams<{ id: string }>();
return <View><Text>Detail ID: {id}</Text></View>;
}Deep linking works automatically with Expo Router - the file path IS the URL scheme.
2. Optimize FlatList performance
FlatList is the primary scrolling container. Misconfigured lists are the number one source of jank.
import { FlatList } from 'react-native';
import { useCallback, memo } from 'react';
const MemoizedItem = memo(({ title }: { title: string }) => (
<View style={styles.item}><Text>{title}</Text></View>
));
export default function OptimizedList({ data }: { data: Item[] }) {
const renderItem = useCallback(({ item }: { item: Item }) => (
<MemoizedItem title={item.title} />
), []);
const keyExtractor = useCallback((item: Item) => item.id, []);
return (
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={keyExtractor}
getItemLayout={(_, index) => ({ length: 80, offset: 80 * index, index })}
windowSize={5}
maxToRenderPerBatch={10}
removeClippedSubviews={true}
initialNumToRender={10}
/>
);
}Always provide
getItemLayoutfor fixed-height items. It eliminates async layout measurement and enables instant scroll-to-index.
3. Create a native module with Expo Modules API
Write native functionality in Swift/Kotlin with a unified TypeScript interface.
npx create-expo-module my-native-module --local// modules/my-native-module/ios/MyNativeModule.swift
import ExpoModulesCore
public class MyNativeModule: Module {
public func definition() -> ModuleDefinition {
Name("MyNativeModule")
Function("getDeviceName") {
return UIDevice.current.name
}
AsyncFunction("fetchData") { (url: String, promise: Promise) in
// async native work
promise.resolve(["status": "ok"])
}
}
}// modules/my-native-module/index.ts
import MyNativeModule from './src/MyNativeModuleModule';
export function getDeviceName(): string {
return MyNativeModule.getDeviceName();
}Prefer Expo Modules API over bare TurboModules for new code - it handles iOS/Android symmetry and codegen automatically.
4. Configure OTA updates with EAS Update
Push JS bundle updates without going through app store review.
# Install and configure
npx expo install expo-updates
eas update:configure
# Publish an update to the preview channel
eas update --branch preview --message "Fix checkout bug"
# Publish to production
eas update --branch production --message "v1.2.1 hotfix"// app.config.ts - updates configuration
{
updates: {
url: 'https://u.expo.dev/your-project-id',
fallbackToCacheTimeout: 0, // 0 = don't block app start waiting for update
},
runtimeVersion: {
policy: 'appVersion', // or 'fingerprint' for automatic compatibility
},
}Use
runtimeVersion.policy: 'fingerprint'to automatically detect native code changes and prevent incompatible JS updates from being applied.
5. Write an Expo config plugin
Customize native project files at prebuild time without ejecting.
// plugins/withCustomScheme.ts
import { ConfigPlugin, withInfoPlist, withAndroidManifest } from 'expo/config-plugins';
const withCustomScheme: ConfigPlugin<{ scheme: string }> = (config, { scheme }) => {
config = withInfoPlist(config, (config) => {
config.modResults.CFBundleURLTypes = [
...(config.modResults.CFBundleURLTypes || []),
{ CFBundleURLSchemes: [scheme] },
];
return config;
});
config = withAndroidManifest(config, (config) => {
const mainActivity = config.modResults.manifest.application?.[0]?.activity?.[0];
if (mainActivity) {
mainActivity['intent-filter'] = [
...(mainActivity['intent-filter'] || []),
{
action: [{ $: { 'android:name': 'android.intent.action.VIEW' } }],
category: [
{ $: { 'android:name': 'android.intent.category.DEFAULT' } },
{ $: { 'android:name': 'android.intent.category.BROWSABLE' } },
],
data: [{ $: { 'android:scheme': scheme } }],
},
];
}
return config;
});
return config;
};
export default withCustomScheme;// app.config.ts - use the plugin
{ plugins: [['./plugins/withCustomScheme', { scheme: 'myapp' }]] }6. Set up EAS Build for production
Cloud builds for iOS and Android without local Xcode/Android Studio.
# Initialize EAS Build
eas build:configure
# Build for both platforms
eas build --platform all --profile production
# Submit to stores
eas submit --platform ios
eas submit --platform android// eas.json
{
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"ios": { "simulator": true }
},
"preview": {
"distribution": "internal"
},
"production": {
"autoIncrement": true
}
},
"submit": {
"production": {
"ios": { "appleId": "you@example.com", "ascAppId": "123456789" },
"android": { "serviceAccountKeyPath": "./google-sa-key.json" }
}
}
}7. Prevent unnecessary re-renders
Use React profiling and memoization strategically - not everywhere.
// Use React DevTools Profiler or why-did-you-render to find actual problems first
// Memoize expensive computations
const sortedItems = useMemo(() =>
items.sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
// Memoize callbacks passed to child components
const handlePress = useCallback((id: string) => {
navigation.navigate('Details', { id });
}, [navigation]);
// Memoize entire components when props are stable
const ExpensiveChart = memo(({ data }: { data: DataPoint[] }) => {
// heavy rendering logic
});
// Use Zustand or Jotai for fine-grained state subscriptions
// instead of React Context which re-renders all consumers
import { create } from 'zustand';
const useStore = create<AppState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));Do not sprinkle
memo()everywhere. Measure first with React DevTools Profiler, then memoize the actual bottleneck.
Gotchas
OTA update applied to incompatible native runtime - EAS Update pushes JS bundles, but if a native module was added or changed since the last app store build, the JS update will crash on load. Use
runtimeVersion.policy: 'fingerprint'to automatically detect native changes and prevent incompatible updates from being served.memo()applied without measuring first - Addingmemo()everywhere is a common premature optimization. It adds object comparison overhead on every render and can cause subtle bugs when object references change unexpectedly. Profile with React DevTools first, then memoize actual bottlenecks.Config plugin modifying already-ejected native files - If native files have been manually edited after
expo prebuild, re-running prebuild overwrites those changes. Either commit all native customizations to config plugins or document explicitly which files are manually managed and must not be regenerated.Expo Router file not a default export - Expo Router requires every route file to have a default export. A named-only export silently breaks routing with an opaque error. Always use
export default function ScreenName()for route files.Context as global state causing full tree re-renders - React Context triggers a re-render in every consumer when any value changes. Using a single large Context object for app state causes cascading re-renders. Use Zustand, Jotai, or split contexts with narrow value shapes for any state accessed by more than a few components.
Error handling
| Error | Cause | Resolution |
|---|---|---|
Invariant Violation: requireNativeComponent |
Native module not linked or pod not installed | Run npx expo prebuild --clean then npx expo run:ios |
Error: No route named "X" exists |
Expo Router file missing or misnamed | Check file exists at app/X.tsx and is a default export |
RuntimeVersion mismatch (EAS Update) |
JS update targets a different native runtime | Set runtimeVersion.policy: 'fingerprint' to auto-detect |
Task :app:mergeDebugNativeLibs FAILED |
Duplicate native libraries on Android | Check for conflicting native deps, use resolutions in package.json |
Metro ENOSPC or slow bundling |
File watcher limit exceeded on Linux/WSL | Increase fs.inotify.max_user_watches to 524288 |
References
For detailed guidance on specific topics, load the relevant reference file:
references/expo-ecosystem.md- Expo SDK modules, config plugins, prebuild, EAS services, and managed vs bare workflow decisionsreferences/navigation.md- React Navigation and Expo Router patterns, deep linking, authentication flows, nested navigators, and modal stacksreferences/native-modules.md- Expo Modules API, TurboModules, JSI, native views, bridging Swift/Kotlin, and the New Architecturereferences/performance.md- Hermes optimization, FlatList tuning, re-render prevention, memory profiling, startup time, and bundle analysisreferences/ota-updates.md- EAS Update workflows, CodePush migration, runtime versioning, rollback strategies, and update policies
Only load a reference file when the current task requires that depth - they are detailed and will consume context.
References
expo-ecosystem.md
Expo Ecosystem Reference
Managed vs Bare Workflow
Managed workflow (recommended): Use app.json/app.config.ts to configure native projects. Run npx expo prebuild to generate ios/ and android/ directories from config. Add native customizations through config plugins. EAS Build handles compilation in the cloud.
Bare workflow: Full access to ios/ and android/ directories. Use when you need custom native code that cannot be expressed through config plugins or Expo Modules. You still get Expo SDK modules, EAS services, and Expo Router - you just manage native projects yourself.
Decision guide:
- Start with managed. Always.
- Move to bare only when you hit a wall that config plugins cannot solve
- Common reasons to go bare: proprietary native SDKs with complex linking, custom app extensions (widgets, watch apps), or heavily modified native build configurations
Expo Prebuild
Prebuild generates native projects from your app.config.ts and config plugins.
# Generate native projects
npx expo prebuild
# Clean regenerate (wipe ios/ and android/ first)
npx expo prebuild --clean
# Generate for one platform only
npx expo prebuild --platform iosKey principle: Treat ios/ and android/ as generated artifacts. Do not manually edit them in managed workflow - use config plugins instead. Add ios/ and android/ to .gitignore in managed projects.
Expo SDK Modules (commonly used)
| Module | Purpose | Install |
|---|---|---|
expo-camera |
Camera capture, barcode scanning | npx expo install expo-camera |
expo-notifications |
Push and local notifications | npx expo install expo-notifications |
expo-image-picker |
Photo/video selection from gallery | npx expo install expo-image-picker |
expo-file-system |
File read/write, downloads | npx expo install expo-file-system |
expo-secure-store |
Encrypted key-value storage | npx expo install expo-secure-store |
expo-location |
GPS, geofencing | npx expo install expo-location |
expo-av |
Audio and video playback | npx expo install expo-av |
expo-haptics |
Haptic feedback | npx expo install expo-haptics |
expo-image |
High-performance image component | npx expo install expo-image |
expo-splash-screen |
Splash screen control | npx expo install expo-splash-screen |
expo-updates |
OTA update client | npx expo install expo-updates |
expo-router |
File-based navigation | npx expo install expo-router |
Always install Expo modules with npx expo install (not npm install) to get the correct version for your SDK.
Config Plugins
Config plugins modify native project files during prebuild. They are the primary mechanism for native customization in managed workflow.
Built-in plugin API
import {
ConfigPlugin,
withInfoPlist, // Modify iOS Info.plist
withEntitlementsPlist, // Modify iOS entitlements
withAndroidManifest, // Modify AndroidManifest.xml
withAppDelegate, // Modify AppDelegate
withMainActivity, // Modify MainActivity
withProjectBuildGradle, // Modify project build.gradle
withAppBuildGradle, // Modify app build.gradle
withPlugins, // Compose multiple plugins
withDangerousMod, // Raw file system access (last resort)
} from 'expo/config-plugins';Plugin structure
// plugins/withMyPlugin.ts
import { ConfigPlugin, withInfoPlist } from 'expo/config-plugins';
interface PluginProps {
apiKey: string;
}
const withMyPlugin: ConfigPlugin<PluginProps> = (config, { apiKey }) => {
return withInfoPlist(config, (config) => {
config.modResults.MY_API_KEY = apiKey;
return config;
});
};
export default withMyPlugin;Usage in app.config.ts
export default {
plugins: [
['./plugins/withMyPlugin', { apiKey: 'abc123' }],
'expo-camera', // Expo modules are also config plugins
['expo-notifications', { icon: './assets/notification-icon.png' }],
],
};Plugin best practices
- Always check if a community plugin exists before writing your own
- Use typed mods (
withInfoPlist,withAndroidManifest) overwithDangerousMod - Test plugins by running
npx expo prebuild --cleanand inspecting the generated native files - Plugins run in order - later plugins can override earlier ones
EAS Services
EAS Build
Cloud build service. No local Xcode or Android Studio needed.
eas build --platform ios --profile development # Dev client build
eas build --platform android --profile preview # Internal distribution
eas build --platform all --profile production # Store buildsEAS Submit
Submit builds directly to App Store Connect and Google Play.
eas submit --platform ios --latest
eas submit --platform android --latestEAS Update
Over-the-air JS bundle updates. See references/ota-updates.md for details.
EAS Metadata (Beta)
Manage app store metadata (screenshots, descriptions) as code.
eas metadata:pull # Pull current metadata from stores
eas metadata:push # Push metadata to storesDevelopment Builds (Dev Client)
Custom development builds that include your native modules but with the Expo Go-like development experience.
# Create a dev client build
eas build --platform ios --profile development
# Start the dev server
npx expo start --dev-clientUse dev clients instead of Expo Go when:
- Your app uses custom native modules
- You need to test config plugin changes
- You are using libraries not included in Expo Go (e.g.,
react-native-maps,@react-native-firebase)
Expo Image (expo-image)
High-performance replacement for React Native's <Image>. Supports:
- Automatic caching with configurable policies
- BlurHash and ThumbHash placeholders
- AVIF, WebP, animated GIF/WebP, SVG
- Content-fit modes (cover, contain, fill, scale-down)
- Recycling for list performance
import { Image } from 'expo-image';
<Image
source={{ uri: 'https://example.com/photo.jpg' }}
placeholder={{ blurhash: 'LGF5]+Yk^6#M@-5c,1J5@[or[Q6.' }}
contentFit="cover"
transition={200}
style={{ width: 300, height: 200 }}
/>Always prefer expo-image over React Native's built-in <Image> for production apps.
native-modules.md
Native Modules Reference
Choosing Your Approach
| Approach | When to use | Complexity |
|---|---|---|
| Expo Modules API | New modules in Expo projects | Low |
| TurboModules (New Arch) | High-performance, RN-only projects | Medium |
| Legacy Bridge Modules | Maintaining old code (avoid for new work) | Medium |
| JSI Direct | Ultra-low-latency (database drivers, crypto) | High |
Expo Modules API
The recommended way to write native modules. Provides a unified Swift/Kotlin API with automatic TypeScript type generation.
Scaffold a new module
# Local module (lives inside your project)
npx create-expo-module my-module --local
# Standalone module (publishable to npm)
npx create-expo-module my-moduleModule definition (iOS - Swift)
// modules/my-module/ios/MyModule.swift
import ExpoModulesCore
public class MyModule: Module {
public func definition() -> ModuleDefinition {
// Module name (used in JS import)
Name("MyModule")
// Synchronous function
Function("add") { (a: Int, b: Int) -> Int in
return a + b
}
// Async function with Promise
AsyncFunction("fetchUser") { (userId: String, promise: Promise) in
DispatchQueue.global().async {
// Network call or heavy work
let user = UserService.fetch(id: userId)
promise.resolve(["name": user.name, "email": user.email])
}
}
// Constants exposed to JS
Constants {
return ["PI": Double.pi, "platform": "ios"]
}
// Events emitted to JS
Events("onProgress", "onComplete")
// Native view component
View(MyNativeView.self) {
Prop("color") { (view, color: UIColor) in
view.backgroundColor = color
}
Events("onTap")
}
}
}Module definition (Android - Kotlin)
// modules/my-module/android/src/main/java/expo/modules/mymodule/MyModule.kt
package expo.modules.mymodule
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
class MyModule : Module() {
override fun definition() = ModuleDefinition {
Name("MyModule")
Function("add") { a: Int, b: Int ->
a + b
}
AsyncFunction("fetchUser") { userId: String ->
val user = UserService.fetch(userId)
mapOf("name" to user.name, "email" to user.email)
}
Constants(
"PI" to Math.PI,
"platform" to "android"
)
Events("onProgress", "onComplete")
View(MyNativeView::class) {
Prop("color") { view: MyNativeView, color: Int ->
view.setBackgroundColor(color)
}
Events("onTap")
}
}
}TypeScript interface
// modules/my-module/index.ts
import MyModule from './src/MyModuleModule';
export function add(a: number, b: number): number {
return MyModule.add(a, b);
}
export async function fetchUser(userId: string): Promise<{ name: string; email: string }> {
return MyModule.fetchUser(userId);
}
export const PI: number = MyModule.PI;Native view component
// modules/my-module/src/MyNativeView.tsx
import { requireNativeView } from 'expo';
import { ViewProps } from 'react-native';
interface MyNativeViewProps extends ViewProps {
color?: string;
onTap?: () => void;
}
const NativeView = requireNativeView<MyNativeViewProps>('MyNativeView');
export function MyNativeView(props: MyNativeViewProps) {
return <NativeView {...props} />;
}TurboModules (New Architecture)
For React Native projects not using Expo, TurboModules provide JSI-based native module access.
Codegen spec
// NativeMyModule.ts (Codegen reads this)
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
add(a: number, b: number): number;
fetchUser(userId: string): Promise<{ name: string; email: string }>;
}
export default TurboModuleRegistry.getEnforcing<Spec>('MyModule');Enable New Architecture
// android/gradle.properties
newArchEnabled=true
// ios - set in Podfile or via env
// ENV['RCT_NEW_ARCH_ENABLED'] = '1'Or in Expo:
// app.json
{
"expo": {
"newArchEnabled": true
}
}JSI (JavaScript Interface)
JSI allows C++ code to be called directly from JavaScript without serialization. Used by libraries like react-native-mmkv, react-native-reanimated, and expo-sqlite.
// C++ JSI host object (simplified)
#include <jsi/jsi.h>
class MyHostObject : public facebook::jsi::HostObject {
public:
facebook::jsi::Value get(
facebook::jsi::Runtime& runtime,
const facebook::jsi::PropNameID& name
) override {
auto methodName = name.utf8(runtime);
if (methodName == "add") {
return facebook::jsi::Function::createFromHostFunction(
runtime, name, 2,
[](facebook::jsi::Runtime& rt, const facebook::jsi::Value& thisVal,
const facebook::jsi::Value* args, size_t count) {
return facebook::jsi::Value(args[0].asNumber() + args[1].asNumber());
}
);
}
return facebook::jsi::Value::undefined();
}
};JSI is low-level. Prefer Expo Modules API or TurboModules unless you need sub-millisecond call overhead.
Common Native Module Patterns
Emitting events to JavaScript
// iOS (Expo Modules)
self.sendEvent("onProgress", ["percent": 0.75])// Android (Expo Modules)
sendEvent("onProgress", mapOf("percent" to 0.75))// JavaScript listener
import { EventEmitter } from 'expo-modules-core';
import MyModule from './MyModuleModule';
const emitter = new EventEmitter(MyModule);
const subscription = emitter.addListener('onProgress', ({ percent }) => {
console.log(`Progress: ${percent * 100}%`);
});
// Clean up
subscription.remove();Handling platform differences
import { Platform } from 'react-native';
export function getDeviceInfo() {
if (Platform.OS === 'ios') {
return MyModule.getIOSSpecificInfo();
} else {
return MyModule.getAndroidSpecificInfo();
}
}Debugging Native Modules
- iOS: Open
ios/*.xcworkspacein Xcode. Add breakpoints in Swift code. UseNSLog()orprint(). - Android: Open
android/in Android Studio. Add breakpoints in Kotlin. UseLog.d("MyModule", message). - Flipper: Inspect bridge messages, network calls, and layout from a desktop app.
- Metro logs: Native errors often surface in Metro terminal as red screens with stack traces.
Migration: Bridge to New Architecture
- Replace
NativeModules.MyModuleimports with TurboModule codegen specs - Convert
RCT_EXPORT_METHODto C++ TurboModule methods (or use Expo Modules API) - Enable New Architecture in gradle.properties / Podfile
- Test all native module calls - synchronous returns work differently under JSI
- Update third-party libraries to New Architecture compatible versions
Check https://reactnative.directory for library compatibility with the New Architecture.
navigation.md
Navigation Reference
Expo Router vs React Navigation
Expo Router (recommended for new projects): File-based routing built on React Navigation. The file system defines your routes. Automatic deep linking, type safety, and web support.
React Navigation (direct usage): Imperative navigation configuration. Use when you need patterns Expo Router does not yet support, or in non-Expo React Native projects.
Both share the same underlying navigation primitives - stacks, tabs, drawers. Expo Router is a convenience layer, not a replacement.
Expo Router Patterns
File structure = route structure
app/
_layout.tsx -> Root layout (wraps all routes)
index.tsx -> / (home screen)
about.tsx -> /about
settings/
_layout.tsx -> Layout for /settings/*
index.tsx -> /settings
profile.tsx -> /settings/profile
details/
[id].tsx -> /details/:id (dynamic segment)
blog/
[...slug].tsx -> /blog/* (catch-all route)
(auth)/
_layout.tsx -> Group layout (no URL segment added)
login.tsx -> /login
register.tsx -> /register
+not-found.tsx -> 404 handlerNavigation actions
import { router, Link, useRouter } from 'expo-router';
// Imperative navigation
router.push('/details/123'); // Push onto stack
router.replace('/home'); // Replace current screen
router.back(); // Go back
router.navigate('/settings'); // Navigate (reuses existing screen if present)
// Declarative navigation
<Link href="/details/123">View Details</Link>
<Link href="/details/123" asChild>
<Pressable><Text>View Details</Text></Pressable>
</Link>Typed routes
// Enable typed routes in app.json
{ "experiments": { "typedRoutes": true } }
// Then get autocomplete and type checking
router.push('/details/123'); // TypeScript validates this path existsLayout navigators
// Stack layout
import { Stack } from 'expo-router';
export default function Layout() {
return (
<Stack screenOptions={{ headerStyle: { backgroundColor: '#f5f5f5' } }}>
<Stack.Screen name="index" options={{ title: 'Home' }} />
<Stack.Screen name="details/[id]" options={{ title: 'Details' }} />
</Stack>
);
}
// Tab layout
import { Tabs } from 'expo-router';
export default function Layout() {
return (
<Tabs>
<Tabs.Screen name="index" options={{ title: 'Feed' }} />
<Tabs.Screen name="search" options={{ title: 'Search' }} />
</Tabs>
);
}
// Drawer layout
import { Drawer } from 'expo-router/drawer';
export default function Layout() {
return (
<Drawer>
<Drawer.Screen name="index" options={{ drawerLabel: 'Home' }} />
</Drawer>
);
}Route groups
Groups organize routes without affecting the URL structure. Use parentheses for group names.
app/
(tabs)/
_layout.tsx -> Tab navigator
home.tsx -> /home (not /(tabs)/home)
profile.tsx -> /profile
(auth)/
_layout.tsx -> Stack navigator for auth flow
login.tsx -> /login
register.tsx -> /registerAuthentication Flow Pattern
// app/_layout.tsx
import { Stack } from 'expo-router';
import { useAuth } from '../hooks/useAuth';
import { Redirect } from 'expo-router';
export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
</Stack>
);
}
// app/(tabs)/_layout.tsx
import { Redirect } from 'expo-router';
import { useAuth } from '../../hooks/useAuth';
export default function TabsLayout() {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) return <Redirect href="/login" />;
return (
<Tabs>
<Tabs.Screen name="home" />
<Tabs.Screen name="profile" />
</Tabs>
);
}Deep Linking
Expo Router (automatic)
Deep linking works out of the box. The file path IS the URL:
app/details/[id].tsxhandlesmyapp://details/123andhttps://myapp.com/details/123
Configure the URL scheme in app.config.ts:
{
scheme: 'myapp',
ios: { associatedDomains: ['applinks:myapp.com'] },
android: {
intentFilters: [{
action: 'VIEW',
autoVerify: true,
data: [{ scheme: 'https', host: 'myapp.com', pathPrefix: '/' }],
}],
},
}React Navigation (manual)
const linking = {
prefixes: ['myapp://', 'https://myapp.com'],
config: {
screens: {
Home: '',
Details: 'details/:id',
Settings: {
screens: {
Profile: 'settings/profile',
},
},
},
},
};
<NavigationContainer linking={linking}>
{/* navigators */}
</NavigationContainer>Nested Navigation Patterns
Tabs containing stacks
The most common pattern: each tab has its own navigation stack.
// app/(tabs)/_layout.tsx
<Tabs>
<Tabs.Screen name="home" />
<Tabs.Screen name="profile" />
</Tabs>
// app/(tabs)/home/_layout.tsx
<Stack>
<Stack.Screen name="index" />
<Stack.Screen name="details/[id]" />
</Stack>Modal presentation
// app/_layout.tsx
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="modal"
options={{ presentation: 'modal', headerShown: false }}
/>
</Stack>
// Navigate to modal from anywhere
router.push('/modal');Navigation State Persistence
Save and restore navigation state for development or user experience:
import AsyncStorage from '@react-native-async-storage/async-storage';
// In React Navigation (manual approach)
const PERSISTENCE_KEY = 'NAVIGATION_STATE';
const [initialState, setInitialState] = useState();
const [isReady, setIsReady] = useState(false);
useEffect(() => {
AsyncStorage.getItem(PERSISTENCE_KEY).then((saved) => {
if (saved) setInitialState(JSON.parse(saved));
setIsReady(true);
});
}, []);
<NavigationContainer
initialState={initialState}
onStateChange={(state) => AsyncStorage.setItem(PERSISTENCE_KEY, JSON.stringify(state))}
>Common Navigation Gotchas
- Screen names must match file names in Expo Router - a typo causes "No route" errors
- Nested navigators reset when switching tabs unless you configure
unmountOnBlur: false - params are NOT reactive - use
useLocalSearchParams(re-renders on change) vsuseGlobalSearchParams(stays current) - Header flickering with nested stacks - set
headerShown: falseon parent when child has its own header - Back behavior on Android - hardware back button follows stack history, not tab history. Customize with
BackHandler
ota-updates.md
OTA Updates Reference
How OTA Updates Work
Over-the-air updates push new JavaScript bundles and assets to users without requiring an app store submission. The native binary stays the same - only the JS layer is replaced.
What CAN be updated OTA:
- JavaScript/TypeScript code
- Images and assets bundled via
require() - JSON configuration files
What CANNOT be updated OTA:
- Native code (Swift, Kotlin, Objective-C, Java)
- Native module additions or removals
- Changes to
app.json/app.config.tsthat affect native configuration - New Expo SDK modules (they include native code)
EAS Update
Expo's official OTA update service. Integrated with EAS Build.
Setup
# Install the updates client
npx expo install expo-updates
# Configure EAS Update
eas update:configure
# This adds to app.json:
# "updates": { "url": "https://u.expo.dev/<project-id>" }
# "runtimeVersion": { "policy": "appVersion" }Publishing updates
# Update a specific branch
eas update --branch production --message "Fix login bug"
# Update with auto-generated message from git
eas update --branch production --auto
# Update only specific platforms
eas update --branch production --platform ios
# Preview what would be updated
eas update --branch preview --message "Test new feature"Branch and channel model
Channels (mapped to build profiles) Branches (where updates live)
----------------------------------- ----------------------------
production channel ------> production branch
preview channel ------> preview branch
development channel ------> development branch- A channel is embedded in the native build at compile time
- A branch is where updates are published
- Channels point to branches. You can remap them without rebuilding.
# Point production channel to a different branch (instant rollback)
eas channel:edit production --branch rollback-v1
# View current mappings
eas channel:listRuntime versioning
Runtime versions ensure JS updates are only applied to compatible native binaries.
| Policy | How it works | When to use |
|---|---|---|
appVersion |
Uses version from app.json |
Simple apps with infrequent native changes |
nativeVersion |
Uses ios.buildNumber / android.versionCode |
When you track native versions manually |
fingerprint |
Auto-hashes all native dependencies and config | Recommended - catches ALL native changes automatically |
| Custom string | You set "runtimeVersion": "1.0.0" manually |
Full control, but error-prone |
// app.config.ts - recommended setup
{
runtimeVersion: {
policy: 'fingerprint', // Auto-detects native code changes
},
updates: {
url: 'https://u.expo.dev/your-project-id',
fallbackToCacheTimeout: 0, // Don't block app start
checkAutomatically: 'ON_LOAD', // Check on every app open
},
}Update lifecycle and behavior
import * as Updates from 'expo-updates';
// Check for updates manually
async function checkForUpdate() {
try {
const update = await Updates.checkForUpdateAsync();
if (update.isAvailable) {
await Updates.fetchUpdateAsync();
// Restart to apply - prompt user first
await Updates.reloadAsync();
}
} catch (error) {
console.log('Update check failed:', error);
}
}
// Listen for automatic update events
Updates.addListener((event) => {
if (event.type === Updates.UpdateEventType.UPDATE_AVAILABLE) {
// An update was downloaded and is ready to apply
// Show a prompt to the user
}
if (event.type === Updates.UpdateEventType.NO_UPDATE_AVAILABLE) {
// Already on the latest version
}
if (event.type === Updates.UpdateEventType.ERROR) {
// Update check or download failed
console.error(event.message);
}
});Update strategies
Strategy 1: Silent background update (most common)
- Check on app launch, download in background
- Apply on next app open (user sees old version until restart)
- Set
fallbackToCacheTimeout: 0
Strategy 2: Forced update
- Check on app launch, block until downloaded
- Set
fallbackToCacheTimeout: 30000(30s timeout) - Use for critical fixes only - degrades UX
Strategy 3: Prompted update
- Check on app launch, notify user when ready
- Let user choose when to restart
- Best balance of UX and update speed
Rollback
# Option 1: Remap channel to a previous branch
eas channel:edit production --branch production-rollback
# Option 2: Republish a previous commit
git checkout <previous-commit>
eas update --branch production --message "Rollback to v1.2.0"
git checkout main
# Option 3: EAS Update automatically rolls back
# If an update crashes on startup, expo-updates falls back to the embedded bundleDebugging updates
# View published updates
eas update:list --branch production
# View update details
eas update:view <update-id>
# Check what runtime version a build expects
eas build:list --platform ios --status finishedCodePush Migration
Microsoft CodePush (App Center) reached end-of-life in March 2025. Migrate to EAS Update.
Migration steps
- Install
expo-updates:npx expo install expo-updates - Remove
react-native-code-pushfrom your project - Remove CodePush wrapper from your root component
- Configure EAS Update in
app.config.ts - Set up branches and channels to match your CodePush deployment targets
- Update CI/CD to use
eas updateinstead ofappcenter codepush release-react
Key differences from CodePush
| Feature | CodePush | EAS Update |
|---|---|---|
| Mandatory updates | codePush.CheckFrequency.ON_APP_RESUME |
fallbackToCacheTimeout + manual check |
| Deployment targets | Production / Staging | Branches + Channels (more flexible) |
| Rollback | Manual via CLI | Automatic crash rollback + manual |
| Runtime compatibility | Manual version targeting | Fingerprint policy (automatic) |
| Hosting | Microsoft Azure | Expo (AWS) |
Best Practices
- Always use fingerprint runtime versioning - it prevents the most common OTA failure (JS/native mismatch)
- Test updates on preview branch first before publishing to production
- Set
fallbackToCacheTimeout: 0in production - never block app start for an update - Monitor update adoption with
eas update:listand analytics events - Keep updates small - only ship changed JS, not the entire bundle (EAS handles this automatically with diffs)
- Have a rollback plan - know how to remap channels before you need to
- Use CI/CD for updates - automate
eas updatein your merge-to-main pipeline
# GitHub Actions example
- name: Publish OTA update
if: github.ref == 'refs/heads/main'
run: eas update --branch production --auto --non-interactive
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} performance.md
Performance Reference
Hermes Engine
Hermes is the default JS engine for React Native (since RN 0.70). It compiles JavaScript to bytecode at build time, reducing startup time and memory usage.
Key benefits
- Bytecode precompilation: No JIT needed at runtime - faster cold starts
- Optimized garbage collector: Generational GC reduces pause times
- Lower memory footprint: ~30-50% less memory than JSC for typical apps
- Hermes bytecode (.hbc): Shipped in the app bundle instead of raw JS
Verify Hermes is enabled
const isHermes = () => !!(global as any).HermesInternal;
console.log('Hermes enabled:', isHermes());Hermes profiling
# Capture a Hermes CPU profile
npx react-native profile-hermes
# Or via Chrome DevTools
# Open chrome://inspect, connect to Hermes, use the Performance tabStartup Time Optimization
Measurement
// Measure TTI (Time to Interactive)
import { PerformanceObserver } from 'react-native-performance';
// Or simple timestamp approach
const appStartTime = global.__APP_START_TIME__; // Set in native code
const jsLoadTime = Date.now();
// In your root component
useEffect(() => {
const tti = Date.now() - appStartTime;
console.log(`TTI: ${tti}ms`);
}, []);Optimization checklist
- Enable Hermes (default in modern RN - verify it is not disabled)
- Reduce JS bundle size: Code split with
require()calls, remove unused dependencies - Defer non-critical initialization: Load heavy modules lazily after first render
- Optimize splash screen: Use
expo-splash-screento control when splash hides - Preload critical data: Start fetching API data in parallel with JS initialization
- Minimize synchronous storage reads: Move
AsyncStorage.getItemcalls out of the render path - Use RAM bundles on Android: For very large apps, enables loading modules on demand
// Lazy module loading
const HeavyChart = React.lazy(() => import('./components/HeavyChart'));
// With Suspense
<Suspense fallback={<ActivityIndicator />}>
<HeavyChart data={chartData} />
</Suspense>FlatList Deep Optimization
Critical props
| Prop | Default | Recommended | Why |
|---|---|---|---|
windowSize |
21 | 5-11 | Reduces off-screen rendering. Lower = less memory, more blank flash |
maxToRenderPerBatch |
10 | 10-20 | Items rendered per frame. Higher = less blank, more frame drops |
initialNumToRender |
10 | 8-15 | Items in first render. Match visible area |
removeClippedSubviews |
false | true (Android) | Detaches off-screen views. Inconsistent on iOS |
getItemLayout |
none | provide always | Eliminates async measurement. Required for scrollToIndex |
keyExtractor |
none | provide always | Stable keys prevent unnecessary re-mounts |
Advanced patterns
// Use FlashList for large lists (drop-in FlatList replacement)
import { FlashList } from '@shopify/flash-list';
<FlashList
data={data}
renderItem={renderItem}
estimatedItemSize={80} // Required - provide best estimate
keyExtractor={keyExtractor}
/>FlashList (by Shopify) uses cell recycling - it reuses view instances instead of creating/destroying them. For lists over 100 items, FlashList typically outperforms FlatList by 5-10x.
Image optimization in lists
// Use expo-image with recyclingKey for list performance
import { Image } from 'expo-image';
const renderItem = ({ item }) => (
<Image
source={{ uri: item.imageUrl }}
recyclingKey={item.id} // Enables view recycling
placeholder={{ blurhash: item.blurhash }}
contentFit="cover"
transition={100}
style={styles.thumbnail}
/>
);Re-render Prevention
Diagnosis
- React DevTools Profiler: Enable "Highlight updates when components render" to visualize re-renders
- why-did-you-render: Library that logs unnecessary re-renders with reasons
npm install @welldone-software/why-did-you-render --save-dev// wdyr.ts (import at app entry before any other imports)
import React from 'react';
if (__DEV__) {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, { trackAllPureComponents: true });
}Common causes and fixes
| Cause | Fix |
|---|---|
| New object/array created every render in props | useMemo for computed values |
| Inline function props | useCallback for functions passed to children |
| Context re-renders all consumers | Split contexts by update frequency, or use Zustand/Jotai |
| Parent re-render cascades | React.memo on expensive child components |
Unstable key prop |
Use stable IDs, never array index for dynamic lists |
State management for performance
// BAD: React Context re-renders ALL consumers on ANY state change
const AppContext = createContext({ user: null, theme: 'light', cart: [] });
// GOOD: Zustand with selectors - components only re-render for their slice
import { create } from 'zustand';
const useStore = create((set) => ({
user: null,
theme: 'light',
cart: [],
setTheme: (theme) => set({ theme }),
}));
// This component ONLY re-renders when theme changes
function ThemeToggle() {
const theme = useStore((state) => state.theme);
const setTheme = useStore((state) => state.setTheme);
// ...
}Memory Profiling
iOS
- Xcode Instruments > Allocations: Track memory growth over time
- Xcode Memory Graph Debugger: Find retain cycles and leaks
Android
- Android Studio Profiler > Memory: Real-time heap tracking
adb shell dumpsys meminfo <package>: Snapshot memory stats
Common memory leaks in React Native
| Leak source | Fix |
|---|---|
| Event listeners not cleaned up | Always return cleanup function from useEffect |
Timers (setInterval) not cleared |
Clear in useEffect cleanup |
| Navigation listeners not removed | Use navigation.addListener return value for cleanup |
| Large images cached without limits | Set cache policies on expo-image |
| Closures capturing stale references | Use refs for mutable values in long-lived callbacks |
Animation Performance
Use the UI thread
// react-native-reanimated runs animations on the UI thread
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
function AnimatedBox() {
const offset = useSharedValue(0);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: offset.value }],
}));
return (
<Animated.View style={[styles.box, animatedStyle]}>
<Pressable onPress={() => { offset.value = withSpring(offset.value + 50); }}>
<Text>Move</Text>
</Pressable>
</Animated.View>
);
}Animation rules
- Never use
Animatedfromreact-nativefor complex animations - usereact-native-reanimated - Avoid
useNativeDriver: false- it runs animations on the JS thread - Gesture handling: use
react-native-gesture-handlerfor 60fps gesture-driven animations - Avoid animating
width/height- usetransform: scaleinstead - Use
layoutanimations from reanimated for enter/exit transitions
Bundle Size Analysis
# React Native CLI
npx react-native bundle --platform ios --dev false --entry-file index.js --bundle-output bundle.js
npx source-map-explorer bundle.js
# Expo
npx expo export --platform ios
# Inspect the generated bundles in dist/Size reduction strategies
- Replace
momentwithdate-fnsor nativeIntl - Replace
lodashwith individual imports or native methods - Use
expo-imageinstead of multiple image libraries - Audit native dependencies - each adds to binary size on both platforms
- Enable ProGuard on Android and bitcode on iOS for release builds
Frequently Asked Questions
What is react-native?
Expert React Native and Expo development skill for building cross-platform mobile apps. Use this skill when creating, debugging, or optimizing React Native projects - Expo setup, native modules, navigation (React Navigation, Expo Router), performance tuning (Hermes, FlatList, re-render prevention), OTA updates (EAS Update, CodePush), and bridging native iOS/Android code. Triggers on mobile app architecture, Expo config plugins, app store deployment, push notifications, and React Native CLI tasks.
How do I install react-native?
Run npx skills add AbsolutelySkilled/AbsolutelySkilled --skill react-native in your terminal. The skill will be immediately available in your AI coding agent.
What AI agents support react-native?
react-native works with claude-code, gemini-cli, openai-codex, mcp. Install it once and use it across any supported AI coding agent.