Loading...
Loading...
Build React Native 0.76+ apps with Expo SDK 52-54. Covers mandatory New Architecture (0.82+/SDK 55+), React 19 changes, SDK 54 breaking changes (expo-av, expo-file-system, Reanimated v4), and Swift iOS template. Prevents 16 documented errors. Use when building Expo apps, migrating to New Architecture, upgrading to SDK 54+, or fixing Fabric, TurboModule, propTypes, expo-updates crashes, or Swift AppDelegate errors.
npx skill4agent add jezweb/claude-skills react-native-expo# Create new Expo app with React Native 0.76+
npx create-expo-app@latest my-app
cd my-app
# Install latest dependencies
npx expo install react-native@latest expo@latest# Check if New Architecture is enabled (should be true by default)
npx expo config --type introspect | grep newArchEnabled# Start Expo dev server
npx expo start
# Press 'i' for iOS simulator
# Press 'a' for Android emulator
# Press 'j' to open React Native DevTools (NOT Chrome debugger!)console.log()# This will FAIL in 0.82+ / SDK 55+:
# gradle.properties (Android)
newArchEnabled=false # ❌ Ignored, build fails
# iOS
RCT_NEW_ARCH_ENABLED=0 # ❌ Ignored, build failspropTypesimport PropTypes from 'prop-types';
function MyComponent({ name, age }) {
return <Text>{name} is {age}</Text>;
}
MyComponent.propTypes = { // ❌ Silently ignored in React 19
name: PropTypes.string.isRequired,
age: PropTypes.number
};type MyComponentProps = {
name: string;
age?: number;
};
function MyComponent({ name, age }: MyComponentProps) {
return <Text>{name} is {age}</Text>;
}# Use React 19 codemod to remove propTypes
npx @codemod/react-19 upgradeforwardRefrefimport { forwardRef } from 'react';
const MyInput = forwardRef((props, ref) => { // ❌ Deprecated
return <TextInput ref={ref} {...props} />;
});function MyInput({ ref, ...props }) { // ✅ ref is a regular prop
return <TextInput ref={ref} {...props} />;
}AppDelegate.swiftAppDelegate.mmios/MyApp/
├── main.m # ❌ Removed
├── AppDelegate.h # ❌ Removed
└── AppDelegate.mm # ❌ Removed// ios/MyApp/AppDelegate.swift ✅
import UIKit
import React
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, ...) -> Bool {
// App initialization
return true
}
}// Add to AppDelegate.swift during migration
import React
import ReactCoreModules
RCTAppDependencyProvider.sharedInstance() // ⚠️ CRITICAL: Must add this!console.log()# console.log() appeared in Metro terminal
$ npx expo start
> LOG Hello from app! # ✅ Appeared here# console.log() does NOT appear in Metro terminal
$ npx expo start
# (no logs shown) # ❌ Removed
# Workaround (temporary, will be removed):
$ npx expo start --client-logs # Shows logs, deprecatedchrome://inspect# ❌ This no longer works:
# Open Dev Menu → "Debug" → Chrome DevTools opens# Press 'j' in CLI or Dev Menu → "Open React Native DevTools"
# ✅ Uses Chrome DevTools Protocol (CDP)
# ✅ Reliable breakpoints, watch values, stack inspection
# ✅ JS Console (replaces Metro logs)# JSC removed from React Native core
# If you still need JSC (rare):
npm install @react-native-community/javascriptcore// ❌ Deep imports deprecated
import Button from 'react-native/Libraries/Components/Button';
import Platform from 'react-native/Libraries/Utilities/Platform';// ✅ Import only from 'react-native'
import { Button, Platform } from 'react-native';// app.json or app.config.js
{
"expo": {
"android": {
// This setting is now IGNORED - edge-to-edge always enabled
"edgeToEdgeEnabled": false // ❌ No effect in SDK 54+
}
}
}react-native-safe-area-contextimport { SafeAreaView } from 'react-native-safe-area-context';
function App() {
return (
<SafeAreaView style={{ flex: 1 }}>
{/* Content respects system bars */}
</SafeAreaView>
);
}display: contents<View style={{ display: 'contents' }}>
{/* This View disappears, but Text still renders */}
<Text>I'm still here!</Text>
</View>boxSizing// Default: padding/border inside box
<View style={{
boxSizing: 'border-box', // Default
width: 100,
padding: 10,
borderWidth: 2
// Total width: 100 (padding/border inside)
}} />
// Content-box: padding/border outside
<View style={{
boxSizing: 'content-box',
width: 100,
padding: 10,
borderWidth: 2
// Total width: 124 (100 + 20 padding + 4 border)
}} />mixBlendModeisolation<View style={{ backgroundColor: 'red' }}>
<View style={{
mixBlendMode: 'multiply', // 16 modes available
backgroundColor: 'blue'
// Result: purple (red × blue)
}} />
</View>
// Prevent unwanted blending:
<View style={{ isolation: 'isolate' }}>
{/* Blending contained within this view */}
</View>multiplyscreenoverlaydarkenlightencolor-dodgecolor-burnhard-lightsoft-lightdifferenceexclusionhuesaturationcolorluminosityoutlineborder<View style={{
outlineWidth: 2,
outlineStyle: 'solid', // solid | dashed | dotted
outlineColor: 'blue',
outlineOffset: 4, // Space between element and outline
outlineSpread: 2 // Expand outline beyond offset
}} />// Load XML drawable at build time
import MyIcon from './assets/my_icon.xml';
<Image
source={MyIcon}
style={{ width: 40, height: 40 }}
/>
// Or with require:
<Image
source={require('./assets/my_icon.xml')}
style={{ width: 40, height: 40 }}
/>useActionStateimport { useActionState } from 'react';
function MyForm() {
const [state, submitAction, isPending] = useActionState(
async (prevState, formData) => {
// Async form submission
const result = await api.submit(formData);
return result;
},
{ message: '' } // Initial state
);
return (
<form action={submitAction}>
<TextInput name="email" />
<Button disabled={isPending}>
{isPending ? 'Submitting...' : 'Submit'}
</Button>
{state.message && <Text>{state.message}</Text>}
</form>
);
}useOptimisticimport { useOptimistic } from 'react';
function LikeButton({ postId, initialLikes }) {
const [optimisticLikes, addOptimisticLike] = useOptimistic(
initialLikes,
(currentLikes, amount) => currentLikes + amount
);
async function handleLike() {
addOptimisticLike(1); // Update UI immediately
await api.like(postId); // Then update server
}
return (
<Button onPress={handleLike}>
❤️ {optimisticLikes}
</Button>
);
}useimport { use } from 'react';
function UserProfile({ userPromise }) {
// Read promise directly during render (suspends if pending)
const user = use(userPromise);
return <Text>{user.name}</Text>;
}jpropTypesnpx @codemod/react-19 upgradeWarning: forwardRef is deprecatedrefforwardRefrefnewArchEnabled=falseFabric component descriptor provider not found for componentTurboModule '[ModuleName]' not foundRCTAppDependencyProvider not foundRCTAppDependencyProvider.sharedInstance()console.log()--client-logsModule not found: react-native/Libraries/...'react-native'reduxredux-thunk@reduxjs/toolkiti18n-jsreact-i18nextnullModule not found: expo-file-system/legacyexpo-file-systemexpo-file-system/legacyexpo-file-systemimport * as FileSystem from 'expo-file-system/legacy';
await FileSystem.writeAsStringAsync(uri, content);import { File } from 'expo-file-system';
const file = new File(uri);
await file.writeString(content);Module not found: expo-avexpo-videoexpo-audioexpo-avexpo-avexpo-av// OLD: expo-av
import { Audio } from 'expo-av';
const { sound } = await Audio.Sound.createAsync(require('./audio.mp3'));
await sound.playAsync();
// NEW: expo-audio
import { useAudioPlayer } from 'expo-audio';
const player = useAudioPlayer(require('./audio.mp3'));
player.play();// OLD: expo-av
import { Video } from 'expo-av';
<Video source={require('./video.mp4')} />
// NEW: expo-video
import { VideoView } from 'expo-video';
<VideoView source={require('./video.mp4')} />| Reanimated Version | Architecture Support | Expo SDK |
|---|---|---|
| v3 | Legacy + New Architecture | SDK 52-54 |
| v4 | New Architecture ONLY | SDK 54+ |
# NativeWind does not support Reanimated v4 yet (as of Jan 2026)
# If using NativeWind, must stay on Reanimated v3
npm install react-native-reanimated@^3react-native-workletsbabel-preset-expohermes::vm::JSObject::putComputed_RJS
hermes::vm::arrayPrototypePush:hermes_enabledexpo-updates# ios/Podfile
use_frameworks! :linkage => :static
ENV['HERMES_ENABLED'] = '1' # ⚠️ CRITICAL: Must be explicit with New Arch + expo-updates# Check current version
npx react-native --version
# Upgrade to 0.81 first (last version with interop layer)
npm install react-native@0.81
npx expo install --fix# Android (gradle.properties)
newArchEnabled=true
# iOS
RCT_NEW_ARCH_ENABLED=1 bundle exec pod install
# Rebuild
npm run ios
npm run android# Replace Redux with Redux Toolkit
npm uninstall redux redux-thunk
npm install @reduxjs/toolkit react-redux
# Replace i18n-js with react-i18next
npm uninstall i18n-js
npm install react-i18next i18next
# Update React Navigation (if old version)
npm install @react-navigation/native@latest# Run on both platforms
npm run ios
npm run android
# Test all features:
# - Navigation
# - State management (Redux)
# - API calls
# - Deep linking
# - Push notifications# Run React 19 codemod
npx @codemod/react-19 upgrade
# Manually verify:
# - Remove all propTypes declarations
# - Remove forwardRef wrappers
# - Update to new hooks (useActionState, useOptimistic)# Only after testing with New Architecture enabled!
npm install react-native@0.82
npx expo install --fix
# Rebuild
npm run ios
npm run android# Follow upgrade helper
# https://react-native-community.github.io/upgrade-helper/
# Select: 0.76 → 0.77
# CRITICAL: Add this line to AppDelegate.swift
RCTAppDependencyProvider.sharedInstance()import { useActionState } from 'react';
function LoginForm() {
const [state, loginAction, isPending] = useActionState(
async (prevState, formData) => {
try {
const user = await api.login(formData);
return { success: true, user };
} catch (error) {
return { success: false, error: error.message };
}
},
{ success: false }
);
return (
<View>
<form action={loginAction}>
<TextInput name="email" placeholder="Email" />
<TextInput name="password" secureTextEntry />
<Button disabled={isPending}>
{isPending ? 'Logging in...' : 'Login'}
</Button>
</form>
{!state.success && state.error && (
<Text style={{ color: 'red' }}>{state.error}</Text>
)}
</View>
);
}// Define prop types with TypeScript
type ButtonProps = {
title: string;
onPress: () => void;
disabled?: boolean;
variant?: 'primary' | 'secondary';
};
function Button({ title, onPress, disabled = false, variant = 'primary' }: ButtonProps) {
return (
<Pressable
onPress={onPress}
disabled={disabled}
style={[styles.button, styles[variant]]}
>
<Text style={styles.text}>{title}</Text>
</Pressable>
);
}// Glowing button with outline and blend mode
function GlowButton({ title, onPress }) {
return (
<Pressable
onPress={onPress}
style={{
backgroundColor: '#3b82f6',
padding: 16,
borderRadius: 8,
// Outline doesn't affect layout
outlineWidth: 2,
outlineColor: '#60a5fa',
outlineOffset: 4,
// Blend with background
mixBlendMode: 'screen',
isolation: 'isolate'
}}
>
<Text style={{ color: 'white', fontWeight: 'bold' }}>
{title}
</Text>
</Pressable>
);
}./scripts/check-rn-version.sh
# Output: ✅ React Native 0.82 - New Architecture mandatory
# Output: ⚠️ React Native 0.75 - Upgrade to 0.76+ recommended// This no longer works in Expo Go (SDK 52+):
{
"jsEngine": "jsc" // ❌ Ignored, Hermes only
}# Must use custom dev client for Google Maps
npx expo install expo-dev-client
npx expo run:androidimport { fetch } from 'expo/fetch';
// Standards-compliant fetch for Workers/Edge runtimes
const response = await fetch('https://api.example.com/data');npm install @react-navigation/native@^7.0.0{
"dependencies": {
"react": "^19.2.3",
"react-native": "^0.81.5",
"expo": "~54.0.31",
"@react-navigation/native": "^7.0.0",
"@reduxjs/toolkit": "^2.0.0",
"react-i18next": "^15.0.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"typescript": "^5.7.0"
}
}npx @codemod/react-19 upgradenpx expo start --client-logsRCTAppDependencyProvider.sharedInstance()reduxredux-thunk@reduxjs/toolkitreact-native/Libraries/*references/new-architecture-errors.mdreferences/react-19-migration.md