Build React Native 0.76+ apps with Expo SDK 52-54, including New Architecture, React 19, SDK 54 breaking changes, and Swift template migration guidance.
Status: Production Ready Last Updated: 2026-01-21 Dependencies: Node.js 20.19.4+, Expo CLI, Xcode 16.1+ (iOS) Latest Versions: [email protected], expo@~54.0.31, [email protected]
This workspace follows a specific Expo + Firebase architecture. When applying this skill here, prefer these repository rules:
App.js; bottom tabs are defined in tabbarview.js.screens/ use PascalCase.WalletContext, StatusContext) rather than Redux.firebaseConfig.js; reuse app and db imports instead of re-initializing.utils/firestoreHelpers.js for retry/timeout/offline fallbacks.cacheManager.js.components/CachedImage.js (expo-image) instead of raw network Image.components/SkeletonLoaders.js.react-native-iap must be guarded with isExpoGo() and should degrade gracefully in Expo Go.# 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
Why this matters:
# Check if New Architecture is enabled (should be true by default)
npx expo config --type introspect | grep newArchEnabled
CRITICAL:
# 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 fails
Migration 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 upgrade
What 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 # ❌ Removed
New 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 here
After (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, deprecated
Solution: 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 opens
New 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/javascriptcore
Expo 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:
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@^3
Migration to v4 (New Architecture only):
react-native-worklets (required for v4)babel-preset-expo (auto-configured)Error: