A step-by-step, version-agnostic React Native upgrade guide with inventory tables, cross-platform alignment commands, verification checks, and rollback procedures.
TL;DR
- Capture a precise baseline tag and inventory before any change.
- Upgrade in scoped steps: RN core → iOS template/toolchain → Android template/toolchain → critical libraries.
- Verify with repeatable Debug-build checks, then graduate to Release builds on physical devices.
- Keep a fast rollback path via git tags, frozen lockfiles, and clean-native rebuilds.
- Gate risky changes with feature flags and staged rollouts; use OTA only when native ABI is stable.
Prerequisites & Assumptions
- Clean git state: working tree committed or stashed; no untracked native artifacts (ios/build, android/.gradle, ios/Pods).
- CI parity: Node, Yarn/npm, Java, Xcode, CocoaPods, Android SDK/NDK versions match your CI agents exactly.
- Platform access: Apple Developer account and Google Play Console access for Release validation and staged rollout.
- Physical devices: Required for Hermes JIT/AOT behavior, billing libraries, and Release performance profiling.
- Convention: All versions, bundle IDs, scheme names, and paths are placeholders; replace with your project values.
Architecture & Runtime Context
Understanding coupling points prevents silent regressions.
- Hermes vs JavaScriptCore: Hermes compiles JS to bytecode ahead-of-time (AOT on iOS, JIT+AOT on Android). Mixed Hermes/JSC settings across platforms cause ABI mismatches and crashes. Keep enableHermes / :hermes_enabled identical on iOS and Android.
- Metro cache: Metro caches module resolution and transforms. Native upgrades (RN version, native modules) invalidate this cache. Always run
npx react-native start --reset-cacheafter native changes. - Native toolchain coupling: Each RN version pins minimum Xcode, CocoaPods, AGP, Gradle wrapper, NDK, minSdkVersion, and iOS deployment target. Drifting from the template causes build failures or runtime crashes. Use the upgrade helper to read the exact requirements for your target version.
Version Compatibility Matrix & Upgrade Helper Usage
Consult the official React Native release notes and the upgrade helper for the authoritative matrix. The table schema below shows what to record for your target version X.Y.Z.
| Component | Source of Truth | Example Value (Constructed) | Notes |
|---|---|---|---|
| React Native | reactnative.dev/versions / Upgrade Helper | 0.76.x | Target version |
| Xcode | Upgrade Helper diff / Release notes | 15.4 | Minimum supported |
| CocoaPods | Release notes | 1.15.2 | Match team/CI |
| iOS Deployment Target | ios/Podfile template diff | 15.1 | platform :ios, '15.1' |
| AGP | android/build.gradle template diff | 8.5.0 | com.android.application |
| Gradle Wrapper | gradle-wrapper.properties template diff | 8.7 | distributionUrl |
| NDK | android/app/build.gradle template diff | 26.1.10909125 | Side-by-side install OK |
| compileSdkVersion | Template diff | 34 | Google Play requirement |
| minSdkVersion | Template diff | 24 | Library minimums may raise this |
| targetSdkVersion | Template diff | 34 | Google Play requirement |
Fetch the template diff for your target version:
# Dry-run CLI upgrade (uses rn-diff-purge under the hood)
npx @react-native-community/cli upgrade --dry-run X.Y.Z
# Or review the web upgrade helper (search "React Native upgrade helper" on reactnative.dev)
Review diffs via git diff after running the upgrade command, or inspect the web helper's file-by-file changes. Prefer manual template alignment when your project has heavy customizations (custom MainApplication, AppDelegate, build scripts, or forked native modules).
Inventory
Capture the exact runtime and build state before any change. Record in a ticket or README-upgrade.md.
| Item | Where / Command | Example Value (Constructed) | Notes |
|---|---|---|---|
| React Native | package.json or node -p "require('./package.json').dependencies['react-native']" | 0.74.5 | Current version |
| Node & Package Managers | node -v, npm -v, yarn -v, pnpm -v | Node 20.12, Yarn 4.1 | Match CI |
| iOS Toolchain | xcodebuild -version, pod --version | Xcode 15.3, CocoaPods 1.15.2 | Team sync |
| Android Toolchain | cat android/gradle/wrapper/gradle-wrapper.properties, grep "com.android.tools.build:gradle" android/build.gradle | Gradle 8.5, AGP 8.4.0 | Align with RN template |
| SDK Levels | android/app/build.gradle, ios/Podfile | minSdk 23, targetSdk 34, compileSdk 34, iOS 15.1 | Store policies |
| Hermes | grep hermes android/app/build.gradle ios/Podfile | enabled | Consistency required |
| Critical Libraries | npm ls --depth=0 | grep -E "navigation|reanimated|gesture|stripe|billing" | @react-navigation/native 6.x, react-native-reanimated 3.x | PeerDependency constraints |
| CI Parity | CI config (.github/workflows, bitrise.yml) | Node 20.12, Xcode 15.3, JDK 17 | Must match local |
Expo notes (constructed):
- Managed workflow: note SDK version (e.g., SDK 51); run
npx expo doctorbefore upgrading. - Bare workflow: treat as standard RN app;
expo prebuild --cleancan validate native config after changes.
Safe Configuration Path
Scope and sequencing keep the blast radius small. Promote changes in observable steps.
1. Choose Target Version & Create Baseline
git checkout -b upgrade/rn-X.Y.Z
git tag pre-rn-X.Y.Z # baseline tag for fast revert
Commit lockfiles (yarn.lock / package-lock.json) if not already tracked. Record the inventory table in the branch description.
2. Define a Narrow Pilot Flow
Select one screen touching JS logic, native UI, and network I/O (constructed example: Settings → Login → Fetch Profile). Define pass/fail checks: no redbox, API 200, token persisted, UI renders.
3. Upgrade React Native Core & Apply Template Diffs
# JavaScript dependency (pick one)
npm install [email protected] --save
# or
yarn add [email protected]
# Apply template diffs via CLI (uses rn-diff-purge)
npx react-native upgrade X.Y.Z
Resolve prompts carefully: prefer keeping project-specific changes. Inspect git diff immediately; commit the RN bump and template alignment separately.
4. Dependency Upgrade Order (PeerDependency-Aware)
Upgrade critical libraries one at a time, committing after each. Read changelogs for minimum RN/Android/iOS requirements.
- react-native (core)
- @react-native-community/* (cli, netinfo, async-storage, etc.)
- Navigation (@react-navigation/*)
- react-native-gesture-handler, react-native-reanimated (tight RN coupling)
- react-native-stripe-sdk, react-native-google-play-billing (store policy coupling)
- Image loaders, analytics, crash reporting, others
iOS Alignment
Align Xcode project, CocoaPods, and signing to the target RN template.
Podfile Configuration (Constructed Example)
# ios/Podfile
require_relative '../node_modules/react-native/scripts/react_native_pods'
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
platform :ios, '15.1' # from template diff
prepare_react_native_project!
target 'YourApp' do
config = use_native_modules!
use_react_native!(
:path => config[:reactNativePath],
:hermes_enabled => true, # match Android
:fabric_enabled => false, # enable when ready
:flipper_configuration => FlipperConfiguration.disabled, # or .enabled for Debug
:app_path => "#{Pod::Config.instance.installation_root}/.."
)
post_install do |installer|
react_native_post_install(installer)
# Workaround for Xcode 15+ signing in CI
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
end
end
end
end
Install & Build Debug
cd ios
pod install --repo-update
cd ..
# Debug build for simulator (CI-friendly)
xcodebuild -workspace ios/YourApp.xcworkspace \
-scheme YourApp \
-configuration Debug \
-sdk iphonesimulator \
-derivedDataPath ios/build \
clean build
Key checks: Deployment target matches template; hermes_enabled consistent; Flipper disabled for Release or aligned to RN version; signing set to CODE_SIGNING_ALLOWED=NO for CI.
Android Alignment
Align Gradle wrapper, AGP, NDK, SDK levels, and Hermes/Fabric flags.
Gradle Wrapper & AGP (Constructed for RN 0.76 Target)
# Update wrapper
cd android
./gradlew wrapper --gradle-version 8.7
cd ..
// android/build.gradle (project level)
plugins {
id 'com.android.application' version '8.5.0' apply false
id 'com.android.library' version '8.5.0' apply false
id 'org.jetbrains.kotlin.android' version '1.9.20' apply false
}
App Module SDK & Hermes (Constructed)
// android/app/build.gradle
android {
namespace "com.yourcompany.yourapp"
compileSdkVersion 34
ndkVersion "26.1.10909125"
defaultConfig {
applicationId "com.yourcompany.yourapp"
minSdkVersion 24
targetSdkVersion 34
versionCode 1
versionName "1.0"
}
// Hermes & Fabric flags (match iOS)
project.ext.react = [
enableHermes: true,
enableFabric: false
]
}
ProGuard / R8 Keep Rules (Constructed)
# android/app/proguard-rules.pro
# Reanimated
-keep class com.swmansion.reanimated.** { *; }
-keepclassmembers class com.swmansion.reanimated.** { *; }
# Stripe
-keep class com.stripe.** { *; }
-dontwarn com.stripe.**
# Google Play Billing
-keep class com.android.billingclient.** { *; }
-keep class com.google.android.play.core.** { *; }
Clean Build Debug
cd android
./gradlew clean assembleDebug --no-daemon
cd ..
Library Migrations
Follow the dependency upgrade order from Safe Configuration Path. For each library:
- Bump version in package.json (respect peerDependency ranges).
- Run
npm install/yarn install. - Run
cd ios && pod install && cd ..if the library has native iOS code. - Build Debug on both platforms.
- Run pilot flow.
- Commit with message:
upgrade: <library>@<version> – <intent>.
PeerDependency watchouts: react-native-reanimated and react-native-gesture-handler often lock to specific RN minor versions. react-native-stripe-sdk and react-native-google-play-billing enforce minSdk / compileSdk floors. Read their package.json peerDependencies before upgrading.
Verification & Diagnostics
Prove the upgrade works with repeatable checks. Start in Debug; graduate to Release on physical devices.
Debug Build & Pilot Flow
# Android
npx react-native run-android
# iOS Simulator
npx react-native run-ios
Execute pilot flow: Launch → Navigate → Login (API 200, token stored) → Fetch Profile (render, pull-to-refresh).
Logs & Diagnostics
# Android: filter RN/JS logs
adb logcat *:S ReactNative:V ReactNativeJS:V
# iOS: recent logs (replace YourApp)
log show --predicate 'process == "YourApp"' --style syslog --last 2m
# Metro cache reset
npx react-native start --reset-cache
Static Checks
npm test -- --watchAll=false
npx tsc --noEmit
npx eslint .
Release Build Smoke Test (Physical Device Required)
# Android Release (includes JS bundle)
./gradlew assembleRelease bundleReleaseJsAndAssets
# iOS Release (CI-friendly, no signing)
xcodebuild -workspace ios/YourApp.xcworkspace \
-scheme YourApp \
-configuration Release \
-sdk iphoneos \
-derivedDataPath ios/build \
CODE_SIGNING_ALLOWED=NO \
clean build
Install Release builds on physical devices. Test Hermes JIT/AOT, billing flows, Stripe, and background/foreground transitions.
Performance Validation (Constructed Examples)
- Cold start: Add
console.time('bootstrap')at app entry;console.timeEnd('bootstrap')in root component useEffect. Extract from adb logcat / os_log. Compare before/after; drift within 10–15% is acceptable. - Frame drops: Enable why-did-you-render in Debug or capture Systrace (
npx react-native profile-hermes). - Bundle size:
npx react-native bundle --dev false --platform ios \
--entry-file index.js \
--bundle-output /tmp/main.jsbundle \
--assets-dest /tmp
# Analyze with hermes-bytecode-analyzer if Hermes enabled
Security & Compliance Checks
- Verify TLS 1.2+ on all network requests (Charles/ProxyMan).
- Confirm no __DEV__ flags, console.log of secrets, or debuggable true in Release AndroidManifest.xml / Entitlements.plist.
- Validate Stripe / Google Play Billing SDK versions meet current store policy minimums.
Verification Table (Adapt to Your Project)
| Check | How to Run | Pass Criteria | Notes |
|---|---|---|---|
| Build Android Debug | ./gradlew assembleDebug | Build succeeds, app launches | Constructed example |
| Build iOS Debug | xcodebuild clean build (simulator) | Simulator launches app | Constructed example |
| Pilot flow works | Manual navigation | No redbox; UI renders; API 200 | Constructed example |
| Logs clean | adb logcat, log show | No crash/severe warnings | Constructed example |
| Static checks pass | npm test, tsc, eslint | Zero failures | Constructed example |
| Build Android Release | ./gradlew assembleRelease bundleReleaseJsAndAssets | APK/AAB produced; installs on device | Physical device |
| Build iOS Release | xcodebuild -configuration Release | .app produced; installs on device | Physical device |
| Release pilot flow | Physical device test | No crashes; billing/Stripe work | Hermes AOT, real network |
| Performance baseline | console.time cold start | Within 10–15% of pre-upgrade | Constructed example |
| Security scan | Proxy TLS, grep __DEV__ | TLS 1.2+; no debug leaks | Constructed example |
Failure Modes & Recovery
Targeted diagnostics for common upgrade symptoms.
| Symptom | Likely Cause | Action & Commands |
|---|---|---|
| CocoaPods spec/CDN error | Stale repo or lockfile | cd ios && rm -rf Pods Podfile.lock && pod repo update && pod install && cd .. |
| Xcode duplicate symbols / Flipper crash | Flipper version mismatch | Disable Flipper in Podfile: :flipper_configuration => FlipperConfiguration.disabled; rebuild; re-enable after aligning. |
| Android duplicate class | Transitive dependency conflict | cd android && ./gradlew :app:dependencies --configuration debugCompileClasspath > debugDeps.txt && cd ..; align versions. |
| Hermes bytecode mismatch / launch crash | Inconsistent Hermes flag or stale caches | Ensure enableHermes / :hermes_enabled identical; cd ios && rm -rf build Pods Podfile.lock && pod install && cd ..; cd android && ./gradlew clean && cd ..; npx react-native start --reset-cache. |
| Metro "Unable to resolve module" | Stale cache / watchman | rm -rf node_modules && npm ci && npx react-native start --reset-cache; watchman watch-del-all. |
| Xcode "Command PhaseScriptExecution failed" (Hermes dSYM) | dSYM generation race | Clean derived data; ensure DEBUG_INFORMATION_FORMAT = dwarf-with-dsym only for Release; disable Hermes dSYM in Debug if persistent. |
| Android "Manifest merger failed" | minSdk / targetSdk conflict | Raise minSdkVersion in android/app/build.gradle to satisfy all libraries; ensure tools:node="replace" for conflicting permissions. |
| ProGuard/R8 crashes (Release only) | Missing keep rules | Add keep rules for reanimated, Stripe, Play Billing (see Android Alignment); test Release build after each addition. |
| Release-only network/TLS failure | Cleartext traffic or cert pinning | Check android:usesCleartextTraffic; verify network_security_config.xml; test with ProxyMan/Charles. |
Rollback Procedures
Local Rollback (Fast)
# Reset to baseline tag
git reset --hard pre-rn-X.Y.Z
# Restore JS deps
npm ci --prefer-offline # or yarn install --frozen-lockfile
# iOS clean rebuild
cd ios
rm -rf Pods Podfile.lock build
pod install
cd ..
# Android clean rebuild
cd android
./gradlew clean
cd ..
CI Rollback
- Revert the upgrade PR/merge commit.
- Trigger pipeline on main; it will use frozen lockfiles and cached toolchains to rebuild the prior version.
- Verify artifact publication (TestFlight, Play Console internal track).
Production Rollback Strategies
- Staged rollout: Pause or decrease rollout percentage in Play Console / App Store Connect when crash rate spikes.
- Feature flags: Disable risky library features remotely (LaunchDarkly, Firebase Remote Config, custom) while keeping app version live.
- OTA JS update (CodePush, Expo Updates, custom): Revert JS bundle only if native ABI is unchanged. If native modules were added/removed or RN version changed native interfaces, ship a hotfix binary.
Post-Rollback Validation
- Prior version builds and launches on both platforms.
- Pilot flow passes; logs clean.
- Crash rates and store reviews return to baseline.
Operations Checklist
Preparation
- [ ] Create branch upgrade/rn-X.Y.Z; tag baseline pre-rn-X.Y.Z.
- [ ] Complete inventory: Node, pkg mgr, RN, iOS/Android toolchains, Hermes, critical libs, CI parity.
- [ ] Define narrow pilot flow with pass/fail checks.
Upgrade Steps
- [ ] Bump react-native; npm install / yarn add.
- [ ] Apply template diffs: npx react-native upgrade X.Y.Z.
- [ ] iOS: pod install; confirm Hermes; build Debug.
- [ ] Android: update Gradle wrapper & AGP; confirm SDK levels; build Debug.
- [ ] Upgrade critical libraries one by one (order per Safe Configuration Path); commit after each.
Verification
- [ ] Launch both platforms; run pilot flow end-to-end.
- [ ] Check logs: no crashes/severe warnings.
- [ ] Run tests: unit, TypeScript, lint.
- [ ] Sanity-check performance (cold start, navigation).
- [ ] Validate REST API, Stripe, Google Play Billing on physical devices.
Stabilization
- [ ] Build Release variants locally once Debug is clean.
- [ ] Re-run pilot and extended smoke tests on Release builds.
- [ ] Document changes and any manual steps.
Recovery Readiness
- [ ] Baseline tag reachable; lockfiles committed.
- [ ] Clean-rebuild commands documented and tested.
- [ ] Feature flags configured for risky areas.
Handoff
- [ ] Share inventory, diffs, verification results.
- [ ] Plan rollout with monitoring and rollback trigger.
CI/CD Integration
Run inventory, verification, and build steps in pipeline. Fail fast on regressions. Publish artifacts for QA.
GitHub Actions Snippet (Constructed)
# .github/workflows/rn-upgrade-validation.yml
name: RN Upgrade Validation
on:
pull_request:
branches: [main]
paths:
- 'package.json'
- 'yarn.lock'
- 'ios/**'
- 'android/**'
jobs:
validate:
runs-on: macos-latest # or self-hosted with Xcode/Android SDK
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # needed for git diff / tags
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20.12'
cache: 'yarn'
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
- name: Cache CocoaPods
uses: actions/cache@v4
with:
path: ios/Pods
key: pods-${{ hashFiles('ios/Podfile.lock') }}
- name: Cache Gradle
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ hashFiles('android/gradle/wrapper/gradle-wrapper.properties', 'android/build.gradle') }}
- name: Install JS deps
run: yarn install --frozen-lockfile
- name: Inventory (baseline capture)
run: |
node -v
yarn -v
node -p "require('./package.json').dependencies['react-native']"
xcodebuild -version
pod --version
cat android/gradle/wrapper/gradle-wrapper.properties
grep "com.android.tools.build:gradle" android/build.gradle
- name: TypeScript & Lint
run: |
npx tsc --noEmit
npx eslint .
- name: Unit Tests
run: yarn test --watchAll=false --ci
- name: iOS Debug Build
run: |
cd ios
pod install --repo-update
cd ..
xcodebuild -workspace ios/YourApp.xcworkspace \
-scheme YourApp \
-configuration Debug \
-sdk iphonesimulator \
-derivedDataPath ios/build \
clean build
- name: Android Debug Build
run: |
cd android
./gradlew assembleDebug --no-daemon
cd ..
- name: Upload Android Debug APK
uses: actions/upload-artifact@v4
with:
name: app-debug
path: android/app/build/outputs/apk/debug/app-debug.apk
# Optional: Release builds on tagged commits or manual dispatch
# - name: Android Release Build
# if: github.event_name == 'workflow_dispatch'
# run: cd android && ./gradlew assembleRelease bundleReleaseJsAndAssets --no-daemon
Bitrise / CircleCI equivalents: Mirror the same steps—cache CocoaPods/Gradle, freeze lockfiles, run typecheck/lint/test, build Debug on both platforms, publish artifacts. Gate Release builds on manual approval or tag push.
Security & Compliance Notes
- Transport security: Verify all network requests use TLS 1.2+; no cleartext traffic in Release. Inspect Network Security Config (Android) and NSAppTransportSecurity (iOS).
- Debug flags: Ensure android:debuggable="false" in Release AndroidManifest.xml; DEBUG=0 / __DEV__=false in JS bundle; no console.log of PII/tokens.
- Billing compliance: Stripe SDK and Google Play Billing Library must meet current store minimums (e.g., Play Billing Library 6+ for Android 14 target). Test purchase flows on physical devices with test accounts.
- Entitlements & permissions: Audit Entitlements.plist and AndroidManifest.xml for unused permissions (location, camera, microphone) after native module upgrades.
- Supply chain: npm audit / yarn audit post-upgrade; pin transitive dependencies if critical CVEs appear.
Conclusion
You now have a practical, low-risk path to upgrade React Native: make the current state explicit with a precise inventory; change one bounded slice at a time, starting with a narrow pilot flow; align iOS and Android templates and toolchains, then upgrade libraries in dependency order; verify outcomes with Debug builds, static checks, Release builds on physical devices, performance baselines, and security scans; recover quickly using committed baseline tag, frozen lockfiles, clean-native rebuilds, and feature flags. Repeat this process for each version step, and you will reduce surprises, shorten failure recovery, and keep shipping while modernizing your app.