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.
Install with Tessl CLI
npx tessl i github:jezweb/claude-skills --skill react-native-expo87
Does it follow best practices?
If you maintain this skill, you can automatically optimize it using the tessl CLI to improve its score:
npx tessl skill review --optimize ./path/to/skillValidation for skill structure
Status: Production Ready Last Updated: 2026-01-21 Dependencies: Node.js 20.19.4+, Expo CLI, Xcode 16.1+ (iOS) Latest Versions: react-native@0.81.5, expo@~54.0.31, react@19.2.3
# 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@latestWhy this matters:
# Check if New Architecture is enabled (should be true by default)
npx expo config --type introspect | grep newArchEnabledCRITICAL:
# 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!)CRITICAL:
console.log() - use DevTools ConsoleSDK Timeline:
What Changed:
Impact:
# This will FAIL in 0.82+ / SDK 55+:
# gradle.properties (Android)
newArchEnabled=false # ❌ Ignored, build fails
# iOS
RCT_NEW_ARCH_ENABLED=0 # ❌ Ignored, build failsMigration Path:
Source: Expo SDK 54 Changelog
What Changed:
React 19 removed propTypes completely. No runtime validation, no warnings - silently ignored.
Before (Old Code):
import 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
};After (Use TypeScript):
type MyComponentProps = {
name: string;
age?: number;
};
function MyComponent({ name, age }: MyComponentProps) {
return <Text>{name} is {age}</Text>;
}Migration:
# Use React 19 codemod to remove propTypes
npx @codemod/react-19 upgradeWhat Changed:
forwardRef no longer needed - pass ref as a regular prop.
Before (Old Code):
import { forwardRef } from 'react';
const MyInput = forwardRef((props, ref) => { // ❌ Deprecated
return <TextInput ref={ref} {...props} />;
});After (React 19):
function MyInput({ ref, ...props }) { // ✅ ref is a regular prop
return <TextInput ref={ref} {...props} />;
}What Changed:
New projects use Swift AppDelegate.swift instead of Objective-C AppDelegate.mm.
Old Structure:
ios/MyApp/
├── main.m # ❌ Removed
├── AppDelegate.h # ❌ Removed
└── AppDelegate.mm # ❌ RemovedNew Structure:
// ios/MyApp/AppDelegate.swift ✅
import UIKit
import React
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, ...) -> Bool {
// App initialization
return true
}
}Migration (0.76 → 0.77): When upgrading existing projects, you MUST add this line:
// Add to AppDelegate.swift during migration
import React
import ReactCoreModules
RCTAppDependencyProvider.sharedInstance() // ⚠️ CRITICAL: Must add this!Source: React Native 0.77 Release Notes
What Changed:
Metro terminal no longer streams console.log() output.
Before (0.76):
# console.log() appeared in Metro terminal
$ npx expo start
> LOG Hello from app! # ✅ Appeared hereAfter (0.77+):
# 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, deprecatedSolution: Use React Native DevTools Console instead (press 'j' in CLI).
Source: React Native 0.77 Release Notes
What Changed:
Old Chrome debugger (chrome://inspect) removed. Use React Native DevTools instead.
Old Method (Removed):
# ❌ This no longer works:
# Open Dev Menu → "Debug" → Chrome DevTools opensNew Method (0.76+):
# 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)Limitations:
Source: React Native 0.79 Release Notes
What Changed: JavaScriptCore (JSC) first-party support removed from React Native 0.81+ core. Moved to community package.
Before (0.78):
After (0.79+ / React Native 0.81+ / SDK 54):
# JSC removed from React Native core
# If you still need JSC (rare):
npm install @react-native-community/javascriptcoreExpo Go:
Note: JSC will eventually be removed entirely from React Native.
Source: Expo SDK 54 Changelog
What Changed: Importing from internal paths will break.
Before (Old Code):
// ❌ Deep imports deprecated
import Button from 'react-native/Libraries/Components/Button';
import Platform from 'react-native/Libraries/Utilities/Platform';After:
// ✅ Import only from 'react-native'
import { Button, Platform } from 'react-native';Source: React Native 0.80 Release Notes
What Changed: Edge-to-edge display is enabled in all Android apps by default in SDK 54 and cannot be disabled.
Impact:
// 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+
}
}
}UI Impact:
Content now extends behind system status bar and navigation bar. You must account for insets manually using react-native-safe-area-context.
Solution:
import { SafeAreaView } from 'react-native-safe-area-context';
function App() {
return (
<SafeAreaView style={{ flex: 1 }}>
{/* Content respects system bars */}
</SafeAreaView>
);
}Source: Expo SDK 54 Changelog
React Native now supports many CSS properties previously only available on web:
display: contentsMakes an element "invisible" but keeps its children in the layout:
<View style={{ display: 'contents' }}>
{/* This View disappears, but Text still renders */}
<Text>I'm still here!</Text>
</View>Use case: Wrapper components that shouldn't affect layout.
boxSizingControl how width/height are calculated:
// 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)
}} />mixBlendMode + isolationBlend layers like Photoshop:
<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>Available modes: multiply, screen, overlay, darken, lighten, color-dodge, color-burn, hard-light, soft-light, difference, exclusion, hue, saturation, color, luminosity
outline PropertiesVisual outline that doesn't affect layout (unlike border):
<View style={{
outlineWidth: 2,
outlineStyle: 'solid', // solid | dashed | dotted
outlineColor: 'blue',
outlineOffset: 4, // Space between element and outline
outlineSpread: 2 // Expand outline beyond offset
}} />Key difference: Outline doesn't change element size or trigger layout recalculations.
Source: React Native 0.77 Release Notes
Use native Android vector drawables (XML) as Image sources:
// 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 }}
/>Benefits:
Constraints:
Source: React Native 0.78 Release Notes
useActionState (replaces form patterns)import { 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>
);
}useOptimistic (optimistic UI updates)import { 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>
);
}use (read promises/contexts during render)import { use } from 'react';
function UserProfile({ userPromise }) {
// Read promise directly during render (suspends if pending)
const user = use(userPromise);
return <Text>{user.name}</Text>;
}Source: React 19 Upgrade Guide
Access:
j in CLIFeatures:
Source: React Native DevTools Announcement
This skill prevents 16 documented issues:
Error: No error - propTypes just doesn't work
Source: React 19 Upgrade Guide
Why It Happens: React 19 removed runtime propTypes validation
Prevention: Use TypeScript instead, run npx @codemod/react-19 upgrade to remove
Error: Warning: forwardRef is deprecated
Source: React 19 Upgrade Guide
Why It Happens: React 19 allows ref as a regular prop
Prevention: Remove forwardRef wrapper, pass ref as prop directly
Error: Build fails with newArchEnabled=false
Source: React Native 0.82 Release Notes
Why It Happens: Legacy architecture completely removed from codebase
Prevention: Migrate to New Architecture before upgrading to 0.82+
Error: Fabric component descriptor provider not found for component
Source: New Architecture Migration Guide
Why It Happens: Component not compatible with New Architecture (Fabric)
Prevention: Update library to New Architecture version, or use interop layer (0.76-0.81)
Error: TurboModule '[ModuleName]' not found
Source: New Architecture Migration Guide
Why It Happens: Native module needs New Architecture support (TurboModules)
Prevention: Update library to support TurboModules, or use interop layer (0.76-0.81)
Error: RCTAppDependencyProvider not found
Source: React Native 0.77 Release Notes
Why It Happens: When migrating from Objective-C to Swift template
Prevention: Add RCTAppDependencyProvider.sharedInstance() to AppDelegate.swift
Error: console.log() doesn't show in terminal
Source: React Native 0.77 Release Notes
Why It Happens: Metro log forwarding removed in 0.77
Prevention: Use React Native DevTools Console (press 'j'), or --client-logs flag (temporary)
Error: Chrome DevTools doesn't connect Source: React Native 0.79 Release Notes Why It Happens: Old Chrome debugger removed in 0.79 Prevention: Use React Native DevTools instead (press 'j')
Error: Module not found: react-native/Libraries/...
Source: React Native 0.80 Release Notes
Why It Happens: Internal paths deprecated, strict API enforced
Prevention: Import only from 'react-native', not deep paths
Error: App crashes on Redux store creation
Source: Redux Toolkit Migration Guide
Why It Happens: Old redux + redux-thunk incompatible with New Architecture
Prevention: Use Redux Toolkit (@reduxjs/toolkit) instead
Error: Translations not updating, or app crashes
Source: Community reports (GitHub issues)
Why It Happens: i18n-js not fully compatible with New Architecture
Prevention: Use react-i18next instead
Error: Android crashes looking for bundle named null
Source: CodePush GitHub Issues
Why It Happens: Known incompatibility with New Architecture
Prevention: Avoid CodePush with New Architecture, or wait for official support
Error: Module not found: expo-file-system/legacy
Source: Expo SDK 54 Changelog, GitHub Issue #39056
Why It Happens: Legacy API removed in SDK 55, must migrate to new File/Directory class API
Prevention: Migrate to new API before upgrading to SDK 55
Migration Timeline:
expo-file-systemexpo-file-system/legacy, new API at expo-file-system (default)Old Code (SDK 54 with legacy import):
import * as FileSystem from 'expo-file-system/legacy';
await FileSystem.writeAsStringAsync(uri, content);New Code (SDK 54+ new API):
import { File } from 'expo-file-system';
const file = new File(uri);
await file.writeString(content);Error: Module not found: expo-av
Source: Expo SDK 54 Changelog, expo-av GitHub
Why It Happens: Package deprecated in SDK 53, removed in SDK 55
Prevention: Migrate to expo-audio and expo-video before SDK 55
Migration Timeline:
expo-video introducedexpo-audio introduced, expo-av deprecatedexpo-av (no patches)expo-av removedMigration - Audio:
// 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();Migration - Video:
// 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')} />Error: Build fails or crashes with Reanimated v4 on Legacy Architecture Source: Expo SDK 54 FYI Why It Happens: Reanimated v4 exclusively requires New Architecture Prevention: Use Reanimated v3 with Legacy Architecture, or migrate to New Architecture first
Version Matrix:
| Reanimated Version | Architecture Support | Expo SDK |
|---|---|---|
| v3 | Legacy + New Architecture | SDK 52-54 |
| v4 | New Architecture ONLY | SDK 54+ |
NativeWind Incompatibility:
# NativeWind does not support Reanimated v4 yet (as of Jan 2026)
# If using NativeWind, must stay on Reanimated v3
npm install react-native-reanimated@^3Migration to v4 (New Architecture only):
react-native-worklets (required for v4)babel-preset-expo (auto-configured)Error:
hermes::vm::JSObject::putComputed_RJS
hermes::vm::arrayPrototypePushSource: GitHub Issue #41824, Medium Post
Verified: Reproduction available
Why It Happens: Expo Updates requires explicit :hermes_enabled flag in Podfile when using New Architecture on iOS
Prevention: Add explicit Hermes flag to ios/Podfile
Conditions:
expo-updatesWorkaround:
# ios/Podfile
use_frameworks! :linkage => :static
ENV['HERMES_ENABLED'] = '1' # ⚠️ CRITICAL: Must be explicit with New Arch + expo-updatesNote: This is a community-sourced finding with reproduction repository, not yet officially documented in Expo changelog.
Why: Can't skip directly to 0.82 if using legacy architecture - you'll lose the interop layer.
# 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 androidCommon incompatibilities:
# 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 androidNew projects (0.77+) use Swift by default. For existing projects:
# 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>
);
}When to use: Form submission with loading/error states
// 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>
);
}When to use: Always (propTypes removed in React 19)
// 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>
);
}When to use: Visual effects without affecting layout (New Architecture only)
check-rn-version.sh - Detects React Native version and warns about architecture requirements
Example Usage:
./scripts/check-rn-version.sh
# Output: ✅ React Native 0.82 - New Architecture mandatory
# Output: ⚠️ React Native 0.75 - Upgrade to 0.76+ recommendedreact-19-migration.md - Detailed React 19 breaking changes and migration steps
new-architecture-errors.md - Common build errors when enabling New Architecture
expo-sdk-52-breaking.md - Expo SDK 52+ specific breaking changes
When Claude should load these: When encountering migration errors, build failures, or detailed React 19 questions
new-arch-decision-tree.md - Decision tree for choosing React Native version
css-features-cheatsheet.md - Complete examples of new CSS properties
JSC Removed from Expo Go:
// This no longer works in Expo Go (SDK 52+):
{
"jsEngine": "jsc" // ❌ Ignored, Hermes only
}Google Maps Removed from Expo Go (SDK 53+):
# Must use custom dev client for Google Maps
npx expo install expo-dev-client
npx expo run:androidPush Notifications Warning: Expo Go shows warnings for push notifications - use custom dev client for production testing.
expo/fetch (WinterCG-compliant):
import { fetch } from 'expo/fetch';
// Standards-compliant fetch for Workers/Edge runtimes
const response = await fetch('https://api.example.com/data');React Navigation v7:
npm install @react-navigation/native@^7.0.0Build Tool Requirements (SDK 54+):
Source: Expo SDK 54 Changelog
{
"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"
}
}Solution: Library not compatible with New Architecture. Check library docs for New Architecture support, or use interop layer (0.76-0.81 only).
Solution: React 19 removed propTypes. Use TypeScript for type checking instead. Run npx @codemod/react-19 upgrade.
Solution: Metro log forwarding removed in 0.77. Use React Native DevTools Console (press 'j') or npx expo start --client-logs (temporary workaround).
Solution: Add RCTAppDependencyProvider.sharedInstance() to AppDelegate.swift. See Swift migration section.
Solution: Use Redux Toolkit instead of legacy redux + redux-thunk. Install @reduxjs/toolkit.
Solution: New Architecture is mandatory in 0.82+. If you need legacy, stay on 0.81 or earlier (not recommended).
Use this checklist to verify your setup:
react-native/Libraries/*)Questions? Issues?
references/new-architecture-errors.md for build errorsreferences/react-19-migration.md for React 19 issuesKnowledge Gap Filled: This skill covers React Native updates from December 2024+ that LLMs won't know about. Without this skill, Claude would suggest deprecated APIs, removed features, and outdated patterns.
fa91c34
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.