Skip to content

Latest commit

 

History

History
990 lines (777 loc) · 32.2 KB

File metadata and controls

990 lines (777 loc) · 32.2 KB

Upgrading React Native

Navigation: ← New Architecture | Dependency Injection →


Table of Contents


Overview

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.


Theory

React Native Upgrade Helper

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:

  1. Select current RN version and target RN version
  2. View diffs for every changed file: package.json, android/, ios/, metro.config.js, etc.
  3. 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.

Safe Upgrade Process

A production-safe upgrade follows these phases:

Phase 1 — Preparation:

  1. Create a dedicated upgrade branch
  2. Audit current dependencies against reactnative.directory
  3. Read release notes for all versions between current and target
  4. Ensure CI green on current version before starting
  5. 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-install

Phase 3 — Native changes:

  • Apply Upgrade Helper diffs for android/ and ios/
  • Update Gradle wrapper, AGP, Kotlin, compile SDK
  • Update Podfile, minimum iOS deployment target
  • Update AppDelegate / MainApplication for new initialization patterns

Phase 4 — Library updates:

  • Update each native-dependent library to compatible version
  • Run npx react-native doctor to 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

Common Gradle and CocoaPods Failures

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-install

Library Compatibility with New Architecture

Starting with RN 0.76, the New Architecture (Fabric + TurboModules) is enabled by default. Library compatibility is the primary upgrade blocker.

Checking compatibility:

  1. Visit reactnative.directory — filter by New Architecture support
  2. Check library GitHub issues/PRs for Fabric/TurboModule support
  3. Look for "codegenConfig" in library's package.json
  4. Test library functionality after enabling New Architecture

Common incompatible patterns:

  • Libraries using deprecated NativeModules bridge directly
  • Libraries with custom RCTBridgeModule without TurboModule migration
  • Libraries using UIWebView or 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

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:

  • NativeModules global is removed — must use TurboModuleRegistry
  • BatchedBridge is removed — no legacy bridge queue
  • Native module initialization happens via JSI directly
  • Some libraries checking NativeModules.X will 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 Compatibility

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 => false

Only disable Hermes as a temporary debugging step — New Architecture requires Hermes.

Post-Upgrade Testing Checklist

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 vs Major Version Jumps

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:

  1. Jump one minor version (e.g., 0.74 → 0.75)
  2. Stabilize — fix issues, merge to main
  3. Jump next minor (0.75 → 0.76)
  4. Repeat until target version reached
  5. Each step should produce a working, deployable build

Code Examples

Upgrade Helper Workflow Script

#!/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"

Gradle Configuration for RN 0.76+

// 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"

Podfile for RN 0.76+

# 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

Library Compatibility Checker

// 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`,
);

Post-Upgrade Smoke Test

// __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();
  });
});

Rollback Strategy

#!/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"

Interview Questions & Answers

Q1: What is the React Native Upgrade Helper, and how do you use it in an upgrade workflow?

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:

  1. Select versions — Enter current version (e.g., 0.74.5) and target version (e.g., 0.76.1)
  2. Review diffs — See changes for every template file: package.json, Gradle files, Podfile, AppDelegate.mm, MainActivity.kt, Metro config, Babel config
  3. Apply changes — Either use npx react-native upgrade as a starting point, then manually apply remaining diffs
  4. 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.


Q2: Describe a step-by-step safe upgrade process for a production React Native app.

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.76

Step 3 — Upgrade JavaScript dependencies:

npx react-native upgrade 0.76.1
npm install

Step 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.zip

2. compileSdkVersion too low:

Android Gradle plugin requires compileSdk 35

Fix: Update android/build.gradle:

compileSdkVersion = 35
targetSdkVersion = 35

3. 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
end

Universal troubleshooting sequence:

  1. Clean everything (Gradle, Pods, Metro cache, node_modules)
  2. Reinstall dependencies
  3. Check Upgrade Helper for missed template changes
  4. 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:

  1. Check reactnative.directory — Filter libraries by "Supports New Architecture" badge

  2. Inspect library source:

    • Has codegenConfig in package.json → TurboModule/Fabric ready
    • Uses TurboModuleRegistry → Migrated
    • Uses NativeModules directly → May work via interop, but not optimal
    • Uses deprecated bridge APIs → Will break in Bridgeless mode
  3. Test under New Architecture:

# android/gradle.properties
newArchEnabled=true

Resolution 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.


Q5: What is Bridgeless Mode, and how does it differ from the standard New Architecture?

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
  • NativeModules global still available for backward compatibility

3. Bridgeless Mode (next evolution):

  • Legacy bridge completely removed
  • All communication via JSI direct bindings
  • NativeModules global does not exist
  • BatchedBridge removed
  • global.RN$Bridgeless === true at 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.


Q6: What Hermes compatibility issues commonly arise during React Native upgrades?

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=false

Interview 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.


Q7: What is your testing checklist after completing a React Native upgrade?

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 .hbc files)

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

Q8: When should you upgrade one minor version at a time versus jumping multiple versions?

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.


Best Practices Summary

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 →