Navigation: ← New Architecture | Dependency Injection →
Upgrading React Native is one of the highest-risk maintenance tasks in mobile development. Each release may change native build tooling (Gradle, CocoaPods, Xcode), JavaScript APIs, Metro bundler configuration, and third-party library compatibility. A structured upgrade process — leveraging the Upgrade Helper, incremental version jumps, and thorough testing — minimizes downtime and production regressions.
This guide covers the upgrade workflow, common failure points, New Architecture considerations, and interview topics essential for senior React Native roles.
The React Native Upgrade Helper is a web tool that shows a file-by-file diff between your current version and target version.
How it works:
- Select current RN version and target RN version
- View diffs for every changed file:
package.json,android/,ios/,metro.config.js, etc. - Apply changes manually or use the CLI upgrade command as a starting point
What it covers:
- Native template changes (Gradle, Podfile, AppDelegate, MainActivity)
- JavaScript config changes (babel, metro, tsconfig)
- Dependency version bumps in template
- New files added or removed between versions
What it does NOT cover:
- Third-party library compatibility
- Custom native module changes
- CI/CD pipeline updates
- App-specific code modifications
Always cross-reference Upgrade Helper diffs with the official release notes for breaking changes not captured in template diffs.
A production-safe upgrade follows these phases:
Phase 1 — Preparation:
- Create a dedicated upgrade branch
- Audit current dependencies against reactnative.directory
- Read release notes for all versions between current and target
- Ensure CI green on current version before starting
- Tag current production release for rollback reference
Phase 2 — JavaScript upgrade:
npx react-native upgrade 0.76.0
# Or manually update package.json and run:
npm install
npx pod-installPhase 3 — Native changes:
- Apply Upgrade Helper diffs for
android/andios/ - Update Gradle wrapper, AGP, Kotlin, compile SDK
- Update Podfile, minimum iOS deployment target
- Update
AppDelegate/MainApplicationfor new initialization patterns
Phase 4 — Library updates:
- Update each native-dependent library to compatible version
- Run
npx react-native doctorto check environment - Fix peer dependency warnings
Phase 5 — Validation:
- Clean build both platforms
- Run full test suite
- Manual QA on critical flows
- Performance benchmark against baseline
Android Gradle failures:
| Error | Cause | Fix |
|---|---|---|
Minimum Gradle version is X |
Gradle wrapper outdated | Update gradle/wrapper/gradle-wrapper.properties |
compileSdkVersion mismatch |
AGP requires higher SDK | Bump compileSdkVersion and targetSdkVersion |
Duplicate class |
Conflicting dependencies | ./gradlew app:dependencies to find conflict |
Could not resolve |
Maven repo or version issue | Check build.gradle repositories block |
Kotlin version incompatibility |
RN requires specific Kotlin | Match Kotlin version in root build.gradle |
Namespace not specified |
AGP 8+ requirement | Add namespace in android/app/build.gradle |
iOS CocoaPods failures:
| Error | Cause | Fix |
|---|---|---|
Unable to find a specification |
Stale pod repo | pod repo update or pod install --repo-update |
[!] CocoaPods could not find compatible versions |
Version conflict | Check Podfile.lock, update conflicting pods |
Undefined symbols for architecture |
Missing framework linkage | Add use_frameworks! or fix modular headers |
deployment target too low |
Pod requires higher iOS | Bump platform :ios in Podfile |
RCT-Folly compile errors |
C++ standard mismatch | Ensure -std=c++20 in Podfile post_install |
Hermes engine build failure |
Xcode version mismatch | Update Xcode to RN-required minimum |
Universal fixes:
# Android clean
cd android && ./gradlew clean && cd ..
# iOS clean
cd ios && rm -rf Pods Podfile.lock build && pod install && cd ..
# Metro cache
npx react-native start --reset-cache
# Full nuclear clean
watchman watch-del-all
rm -rf node_modules
rm -rf ios/Pods ios/Podfile.lock
rm -rf android/.gradle android/app/build
npm install
npx pod-installStarting with RN 0.76, the New Architecture (Fabric + TurboModules) is enabled by default. Library compatibility is the primary upgrade blocker.
Checking compatibility:
- Visit reactnative.directory — filter by New Architecture support
- Check library GitHub issues/PRs for Fabric/TurboModule support
- Look for
"codegenConfig"in library'spackage.json - Test library functionality after enabling New Architecture
Common incompatible patterns:
- Libraries using deprecated
NativeModulesbridge directly - Libraries with custom
RCTBridgeModulewithout TurboModule migration - Libraries using
UIWebViewor other deprecated APIs - Libraries with pinned old Gradle/NDK versions
Interop layer: RN provides a compatibility layer that allows legacy bridge modules to work under New Architecture. However, performance benefits are reduced, and the interop layer may be removed in future versions.
Bridgeless Mode is the final evolution of the New Architecture where the legacy bridge is completely removed:
| Mode | Bridge | TurboModules | Fabric | Status |
|---|---|---|---|---|
| Legacy | Yes | No | No | Deprecated |
| New Architecture | Interop | Yes | Yes | Default (0.76+) |
| Bridgeless | No | Yes | Yes | Experimental → Stable |
What changes in Bridgeless:
NativeModulesglobal is removed — must useTurboModuleRegistryBatchedBridgeis removed — no legacy bridge queue- Native module initialization happens via JSI directly
- Some libraries checking
NativeModules.Xwill break
Detecting Bridgeless at runtime:
const isBridgeless = global.RN$Bridgeless === true;Migration impact: Apps and libraries must replace all NativeModules imports with TurboModuleRegistry.get(). React Native core modules are already migrated; third-party libraries are the main concern.
Hermes is the default JavaScript engine since RN 0.70 and is required for New Architecture:
Common Hermes issues during upgrade:
| Issue | Symptom | Fix |
|---|---|---|
| Intl not available | Intl is undefined |
Enable hermesFlags: ["-O", "-enable-intl"] or use intl-pluralrules polyfill |
| Proxy not supported | MobX/Immer crashes | Upgrade library or disable Hermes temporarily |
eval() usage |
Runtime crash | Remove eval; Hermes doesn't support it |
| Different error messages | Tests fail on error text | Update test assertions |
| Bytecode mismatch | App crashes on startup | Clean build; regenerate bytecode |
Verifying Hermes:
const isHermes = () => !!global.HermesInternal;
console.log('JS Engine:', isHermes() ? 'Hermes' : 'JSC');Disabling Hermes (not recommended):
# android/gradle.properties
hermesEnabled=false# ios/Podfile
:hermes_enabled => falseOnly disable Hermes as a temporary debugging step — New Architecture requires Hermes.
After upgrading, validate these areas systematically:
Build verification:
- Clean debug build — Android
- Clean debug build — iOS
- Release build — Android (APK/AAB)
- Release build — iOS (Archive)
- Hermes bytecode compilation succeeds
Core functionality:
- App launches (cold start)
- App launches (warm start from background)
- Navigation between all screens
- Deep linking (universal links + custom scheme)
- Push notification receipt and tap handling
- Authentication flow (login, logout, token refresh)
Native features:
- Camera / photo library
- Location services
- Biometric authentication
- File system / document picker
- In-app purchases / payments
- Background tasks
Performance baseline:
- Cold start time (TTI) — compare to pre-upgrade
- Memory usage at idle
- Scroll FPS on complex lists
- Bundle size (APK/IPA)
Platform-specific:
- Android: test on API 24 (min) and API 34 (target)
- iOS: test on minimum deployment target device
- Dark mode / light mode
- Accessibility (VoiceOver / TalkBack)
- RTL layout (if supported)
Minor version upgrade (e.g., 0.75 → 0.76):
- Typically safe to jump one minor at a time
- Template changes are incremental
- Breaking changes documented in release notes
- Recommended cadence: every 2-3 months
Major version jump (e.g., 0.72 → 0.76):
- Accumulated breaking changes across multiple releases
- Higher risk of library incompatibility
- Upgrade Helper diffs are larger and harder to merge
- Strongly recommend stepping through each minor version
Decision framework:
| Factor | One minor at a time | Multi-version jump |
|---|---|---|
| Custom native code | Required | Risky |
| Many native dependencies | Required | Very risky |
| JS-only app | Either works | Acceptable |
| Behind by 3+ versions | Step through | Never jump all at once |
| Pre-production app | Either works | Acceptable |
| Production with users | One at a time | Never |
Recommended approach:
- Jump one minor version (e.g., 0.74 → 0.75)
- Stabilize — fix issues, merge to main
- Jump next minor (0.75 → 0.76)
- Repeat until target version reached
- Each step should produce a working, deployable build
#!/bin/bash
# scripts/upgrade-rn.sh — Structured upgrade helper
set -e
CURRENT_VERSION=$(node -p "require('./package.json').dependencies['react-native']")
TARGET_VERSION=${1:-"latest"}
echo "Current RN version: $CURRENT_VERSION"
echo "Target RN version: $TARGET_VERSION"
echo ""
echo "Step 1: Create upgrade branch"
git checkout -b "upgrade/rn-${TARGET_VERSION}"
echo "Step 2: Run react-native upgrade"
npx react-native upgrade "$TARGET_VERSION"
echo "Step 3: Install dependencies"
npm install
echo "Step 4: Install iOS pods"
npx pod-install
echo "Step 5: Run doctor"
npx react-native doctor
echo "Step 6: Clean builds"
cd android && ./gradlew clean && cd ..
rm -rf ios/build
echo ""
echo "Upgrade scaffold complete. Next steps:"
echo " 1. Review Upgrade Helper diffs: https://react-native-community.github.io/upgrade-helper/"
echo " 2. Update incompatible libraries"
echo " 3. Fix native build errors"
echo " 4. Run test suite"
echo " 5. Manual QA on device"// android/build.gradle
buildscript {
ext {
buildToolsVersion = "35.0.0"
minSdkVersion = 24
compileSdkVersion = 35
targetSdkVersion = 35
ndkVersion = "27.1.12297006"
kotlinVersion = "2.0.21"
}
dependencies {
classpath("com.android.tools.build:gradle:8.7.2")
classpath("com.facebook.react:react-native-gradle-plugin")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
}
}# android/gradle.properties
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=512m
android.useAndroidX=true
newArchEnabled=true
hermesEnabled=true// android/app/build.gradle
android {
namespace "com.myapp"
compileSdk rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.myapp"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
}
}
dependencies {
implementation("com.facebook.react:react-android")
if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android")
}
}
apply plugin: "com.facebook.react"# ios/Podfile
require Pod::Executable.execute_command(
'node',
['-p', 'require.resolve("react-native/scripts/react_native_pods.rb", {paths: [process.argv[1]]})', __dir__]
).strip
platform :ios, '15.1'
prepare_react_native_project!
ENV['RCT_NEW_ARCH_ENABLED'] = '1'
linkage = ENV['USE_FRAMEWORKS']
if linkage != nil
Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
use_frameworks! :linkage => linkage.to_sym
end
target 'MyApp' do
config = use_native_modules!
use_react_native!(
:path => config[:reactNativePath],
:hermes_enabled => true,
:app_path => "#{Pod::Config.instance.installation_root}/.."
)
post_install do |installer|
react_native_post_install(
installer,
config[:reactNativePath],
:mac_catalyst_enabled => false
)
end
end// scripts/checkLibraryCompat.ts
import { execSync } from 'child_process';
import packageJson from '../package.json';
interface CompatResult {
name: string;
version: string;
hasNativeCode: boolean;
hasCodegen: boolean;
status: 'ok' | 'warning' | 'unknown';
}
function checkLibraryCompat(): CompatResult[] {
const deps = {
...packageJson.dependencies,
...packageJson.devDependencies,
};
return Object.entries(deps)
.filter(([name]) => name.startsWith('react-native-') || name.startsWith('@react-native'))
.map(([name, version]) => {
let hasNativeCode = false;
let hasCodegen = false;
try {
const libPkg = require(`${name}/package.json`);
hasCodegen = !!libPkg.codegenConfig;
hasNativeCode = !!(
libPkg.codegenConfig ||
libPkg.ios ||
libPkg.android ||
libPkg.podspecPath
);
} catch {
// Library may not expose package.json at root
}
return {
name,
version: version as string,
hasNativeCode,
hasCodegen,
status: hasNativeCode && !hasCodegen ? 'warning' : 'ok',
};
});
}
const results = checkLibraryCompat();
console.table(results);
console.log(
`\n⚠️ ${results.filter((r) => r.status === 'warning').length} libraries may need New Architecture updates`,
);// __tests__/upgradeSmoke.test.ts
import { Platform } from 'react-native';
describe('Post-upgrade smoke tests', () => {
it('should have correct RN version', () => {
const { version } = require('react-native/Libraries/Core/ReactNativeVersion');
expect(version.major).toBe(0);
expect(version.minor).toBeGreaterThanOrEqual(76);
});
it('should have Hermes enabled', () => {
expect(global.HermesInternal).toBeDefined();
});
it('should detect New Architecture', () => {
const isNewArch =
global.RN$Bridgeless === true ||
global.__turboModuleProxy != null;
expect(isNewArch).toBe(true);
});
it('should resolve TurboModules', () => {
const { TurboModuleRegistry } = require('react-native');
const platformConstants = TurboModuleRegistry.get('PlatformConstants');
expect(platformConstants).toBeDefined();
expect(platformConstants.getConstants().reactNativeVersion).toBeDefined();
});
it('should render without bridge errors', () => {
const React = require('react');
const { Text } = require('react-native');
const { render } = require('@testing-library/react-native');
const { getByText } = render(<Text>Upgrade OK</Text>);
expect(getByText('Upgrade OK')).toBeTruthy();
});
});#!/bin/bash
# scripts/rollback-upgrade.sh
ROLLBACK_TAG=${1:-"pre-upgrade"}
echo "Rolling back to tag: $ROLLBACK_TAG"
git checkout "$ROLLBACK_TAG" -- package.json package-lock.json
git checkout "$ROLLBACK_TAG" -- android/ ios/
npm install
npx pod-install
cd android && ./gradlew clean && cd ..
echo "Rollback complete. Verify with: npx react-native run-android"Answer:
The React Native Upgrade Helper (react-native-community.github.io/upgrade-helper) is a community tool that generates file-by-file diffs between any two React Native versions.
Workflow:
- Select versions — Enter current version (e.g., 0.74.5) and target version (e.g., 0.76.1)
- Review diffs — See changes for every template file:
package.json, Gradle files, Podfile,AppDelegate.mm,MainActivity.kt, Metro config, Babel config - Apply changes — Either use
npx react-native upgradeas a starting point, then manually apply remaining diffs - Cross-reference release notes — Upgrade Helper shows template changes; release notes document API breaking changes
What Upgrade Helper shows:
- Lines added/removed in each file
- New files introduced in the target version
- Files deleted in the target version
- Raw file content for copy-paste
Limitations to mention in interviews:
- Only covers React Native core template — not third-party libraries
- Doesn't detect custom native code conflicts
- Doesn't update your app's business logic
- May not reflect latest patch version nuances
Best practice: Use Upgrade Helper diffs alongside npx react-native upgrade, not instead of it. The CLI handles package.json bumps; Upgrade Helper ensures you don't miss native template changes.
Answer:
Step 1 — Pre-upgrade audit (1-2 days):
- Tag current production release (
git tag v1.2.3-pre-upgrade) - Run
npx react-native doctor— fix environment issues first - Audit all dependencies on reactnative.directory for New Architecture compatibility
- Read release notes for every version between current and target
- Ensure CI is green and test coverage is adequate
Step 2 — Create upgrade branch:
git checkout -b upgrade/rn-0.76Step 3 — Upgrade JavaScript dependencies:
npx react-native upgrade 0.76.1
npm installStep 4 — Apply native template changes:
- Open Upgrade Helper diffs
- Update
android/build.gradle,gradle-wrapper.properties,app/build.gradle - Update
ios/Podfile,AppDelegate, deployment target - Run
npx pod-install
Step 5 — Update third-party libraries:
- Update each native-dependent library to compatible version
- Check for peer dependency warnings
- Replace deprecated APIs in app code
Step 6 — Fix build errors:
- Android:
./gradlew clean && npx react-native run-android - iOS:
cd ios && pod install && cd .. && npx react-native run-ios - Iterate on Gradle/Pod errors until clean build
Step 7 — Test thoroughly:
- Automated test suite
- Manual QA checklist (navigation, auth, push, deep links, payments)
- Performance benchmark (startup time, memory, FPS)
- Test on minimum OS versions
Step 8 — Staged rollout:
- Deploy to internal testers first
- Beta track on Play Store / TestFlight
- Monitor crash reports (Sentry/Crashlytics)
- Gradual production rollout (10% → 50% → 100%)
Step 9 — Merge and document:
- Merge upgrade branch to main
- Document any breaking changes for the team
- Update CI/CD configs if needed
Q3: What are the most common Gradle and CocoaPods failures during a React Native upgrade, and how do you fix them?
Answer:
Top Android Gradle failures:
1. Gradle/AGP version mismatch:
Minimum supported Gradle version is 8.11.1. Current version is 8.6.
Fix: Update gradle/wrapper/gradle-wrapper.properties:
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip2. compileSdkVersion too low:
Android Gradle plugin requires compileSdk 35
Fix: Update android/build.gradle:
compileSdkVersion = 35
targetSdkVersion = 353. Duplicate class / dependency conflict:
Duplicate class com.google.android.gms.internal found in modules
Fix: Run ./gradlew app:dependencies, identify conflicting versions, force resolution:
configurations.all {
resolutionStrategy {
force 'com.google.android.gms:play-services-base:18.3.0'
}
}4. Kotlin version mismatch:
Module was compiled with an incompatible version of Kotlin
Fix: Match Kotlin version in root build.gradle to RN requirement.
Top iOS CocoaPods failures:
1. Spec not found:
Unable to find a specification for 'RNSomeLib'
Fix: pod install --repo-update or pod repo update
2. Version conflict:
CocoaPods could not find compatible versions for pod "RCT-Folly"
Fix: Delete Podfile.lock, run pod install --repo-update
3. Deployment target too low:
Specs satisfying the 'React-Core' dependency were found, but they required a higher minimum deployment target
Fix: Bump platform :ios, '15.1' in Podfile
4. C++ compilation errors (RCT-Folly): Fix: Ensure post_install hook sets C++20:
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['CLANG_CXX_LANGUAGE_STANDARD'] = 'c++20'
end
end
endUniversal troubleshooting sequence:
- Clean everything (Gradle, Pods, Metro cache, node_modules)
- Reinstall dependencies
- Check Upgrade Helper for missed template changes
- Search GitHub issues for the specific error + RN version
Q4: How do you assess and resolve library compatibility issues with the New Architecture during an upgrade?
Answer:
Library compatibility is the #1 blocker for New Architecture adoption:
Assessment process:
-
Check reactnative.directory — Filter libraries by "Supports New Architecture" badge
-
Inspect library source:
- Has
codegenConfigin package.json → TurboModule/Fabric ready - Uses
TurboModuleRegistry→ Migrated - Uses
NativeModulesdirectly → May work via interop, but not optimal - Uses deprecated bridge APIs → Will break in Bridgeless mode
- Has
-
Test under New Architecture:
# android/gradle.properties
newArchEnabled=trueResolution strategies:
| Library Status | Action |
|---|---|
| Officially supports New Architecture | Update to latest version |
| Open PR for support, not merged | Use fork or wait; disable New Architecture temporarily |
| Abandoned, no New Architecture support | Find alternative library or fork and migrate |
| Pure JS library (no native code) | No action needed — works automatically |
Common migration patterns for custom libraries:
// Before (Legacy)
import { NativeModules } from 'react-native';
const { MyModule } = NativeModules;
// After (TurboModule)
import { TurboModuleRegistry } from 'react-native';
import type { Spec } from './NativeMyModule';
const MyModule = TurboModuleRegistry.getEnforcing<Spec>('MyModule');Interop layer (temporary): Legacy bridge modules continue working under New Architecture via an interop layer. However:
- Performance benefits are reduced
- Interop may be removed in future RN versions
- Some edge cases (sync methods, constants) behave differently
Interview tip: Mention that RN 0.76+ enables New Architecture by default, so upgrades now require library compatibility assessment, not optional evaluation.
Answer:
React Native's architecture evolution has three stages:
1. Legacy Architecture:
- Async bridge between JS and native
- All native modules loaded at startup
- JSON serialization on every call
2. New Architecture (current default, RN 0.76+):
- Fabric renderer + TurboModules via JSI
- Legacy bridge still present as interop layer for unmigrated modules
NativeModulesglobal still available for backward compatibility
3. Bridgeless Mode (next evolution):
- Legacy bridge completely removed
- All communication via JSI direct bindings
NativeModulesglobal does not existBatchedBridgeremovedglobal.RN$Bridgeless === trueat runtime
Key differences:
| Feature | New Architecture | Bridgeless |
|---|---|---|
| Legacy bridge | Present (interop) | Removed |
NativeModules |
Available | Removed |
| TurboModuleRegistry | Available | Required |
| Startup performance | Fast | Fastest |
| Library compatibility | Interop helps | All must be migrated |
Impact on code:
// This breaks in Bridgeless:
import { NativeModules } from 'react-native';
NativeModules.MyCustomModule.doSomething();
// This works in Bridgeless:
import { TurboModuleRegistry } from 'react-native';
TurboModuleRegistry.getEnforcing<Spec>('MyCustomModule').doSomething();Migration timeline: Bridgeless is experimental in RN 0.73-0.75, becoming stable in 0.76+. Teams should migrate custom native modules to TurboModules now to prepare.
Answer:
Hermes is the default JS engine and required for New Architecture. Upgrade-related issues:
1. Intl API missing:
// Crashes on Hermes without polyfill
new Intl.DateTimeFormat('en-US').format(new Date());Fix: Use intl-pluralrules polyfill or enable Intl in Hermes build flags:
// android/app/build.gradle
react {
hermesFlags = ["-O", "-enable-intl"]
}2. Proxy unsupported (older Hermes): Libraries like MobX 5 or older Immer versions use Proxy, which early Hermes versions didn't support. Fix: Upgrade to MobX 6+ / Immer 9+ with Hermes-compatible builds.
3. Different JavaScript semantics:
- No
eval()support - Stricter regex handling
- Slightly different error stack traces (breaks snapshot tests)
4. Bytecode compilation failures:
hermesc: error: Invalid syntax at line 42
Fix: Ensure Babel transforms are compatible; check for unsupported syntax in dependencies.
5. Engine detection in tests:
// Tests may behave differently
const isHermes = !!global.HermesInternal;
if (isHermes) {
// Hermes-specific test adjustments
}Debugging Hermes issues:
# Check Hermes version
adb logcat | grep Hermes
# Disable Hermes temporarily to isolate
# android/gradle.properties: hermesEnabled=falseInterview key point: Always develop and test on Hermes — don't develop on JSC and expect Hermes to work in production. Behavior differences between engines cause subtle production bugs.
Answer:
A structured post-upgrade checklist prevents production regressions:
Build verification:
- Android debug build compiles and runs
- Android release build (AAB) compiles
- iOS debug build compiles and runs
- iOS release archive succeeds
- Hermes bytecode generation succeeds (check for
.hbcfiles)
Automated tests:
- Unit tests pass (
npm test) - Integration tests pass
- E2E tests pass (Detox/Maestro)
- TypeScript compilation clean (
tsc --noEmit) - Lint passes (
eslint .)
Core app flows (manual QA):
- Cold start → home screen loads
- Authentication (login, logout, session persistence)
- Navigation (all screens, back gesture, tab switching)
- Deep linking (custom scheme + universal links)
- Push notifications (receive, tap, foreground display)
- Offline mode and reconnection
Native module verification:
- Camera / image picker
- Geolocation
- Biometrics (Face ID / fingerprint)
- File system operations
- Third-party SDKs (analytics, crash reporting, payments)
Performance comparison (vs pre-upgrade baseline):
- Cold start time (TTI)
- Memory usage at idle (5 min after launch)
- Scroll FPS on primary list screen
- APK/IPA size delta
- JS bundle size
Platform-specific:
- Android: min SDK device (API 24) + target SDK (API 34)
- iOS: minimum deployment target device
- Dark mode / light mode rendering
- Accessibility (VoiceOver, TalkBack, font scaling)
- RTL layout (if applicable)
Monitoring post-release:
- Crash-free rate stable (Sentry/Crashlytics)
- ANR rate not increased (Android)
- No new JS error spikes
- User reviews don't mention crashes
Answer:
The decision depends on risk tolerance, app complexity, and how far behind you are:
Upgrade one minor at a time when:
- App is in production with active users
- App has custom native modules (TurboModule migration needed)
- App depends on many native third-party libraries
- You're upgrading 2+ minor versions (e.g., 0.72 → 0.74)
- Team lacks recent upgrade experience
- CI/CD and release pipeline are complex
Process:
0.72 → 0.73 (stabilize, deploy) → 0.74 (stabilize, deploy) → 0.75 → 0.76
Each step produces a deployable build merged to main.
Jump multiple versions when:
- App is pre-production or internal-only
- App is JavaScript-only (no custom native code)
- Only one minor version behind (e.g., 0.75 → 0.76)
- Libraries are all confirmed compatible with target version
- Strong automated test coverage exists
Never do:
- Jump 3+ minor versions in production (0.72 → 0.76 directly)
- Upgrade during a critical release window
- Upgrade without reading release notes for skipped versions
- Upgrade without a rollback plan
Risk matrix:
| App Profile | 1 minor behind | 2 minors behind | 3+ minors behind |
|---|---|---|---|
| JS-only, pre-prod | Jump OK | Jump OK | Step through |
| JS-only, production | Jump OK | Step through | Step through |
| Native modules, production | Jump OK | Step through | Step through |
| Heavy native SDKs | Step through | Step through | Step through |
Recommended cadence: Upgrade every 2-3 months (one minor version), staying within 1-2 versions of latest. Falling more than 3 versions behind makes upgrades exponentially harder due to accumulated breaking changes.
| Practice | Reason |
|---|---|
| Use Upgrade Helper for every upgrade | Catch all native template changes |
| Upgrade one minor version at a time in production | Reduce risk, isolate breaking changes |
| Tag pre-upgrade release for rollback | Fast recovery if upgrade fails |
| Audit library compatibility before upgrading | New Architecture is default since 0.76 |
Run react-native doctor before starting |
Fix environment issues proactively |
| Clean all caches after native changes | Stale caches cause misleading build errors |
| Test on Hermes during development | Production engine; JSC hides Hermes bugs |
| Benchmark performance before and after | Quantify upgrade impact |
| Deploy via staged rollout | Catch production issues before full release |
| Document breaking changes for the team | Knowledge transfer and onboarding |
| Keep dependencies updated between RN upgrades | Smaller dependency jumps during RN upgrade |
| Maintain an upgrade runbook | Repeatable process reduces upgrade time |
Navigation: ← New Architecture | Dependency Injection →