Govind Bagla

Nov 15, 2025 • 5 min read

Upgrading React Native: A Pragmatic Approach

Upgrading React Native: A Pragmatic Approach

When we recently upgraded from version 0.74 to 0.81, the journey taught us valuable lessons about taking incremental steps over quick fixes. Here’s our story and the approach that worked.

Why the Upgrade Helper Wasn’t Enough

We started, like many developers do, with the React Native Upgrade Helper. While this tool is excellent for smaller projects, it fell short for our large-scale application. With numerous dependencies, custom native modules, and complex integrations, we needed a more hands-on approach.

The upgrade helper shows you all the file changes between versions, but it doesn’t tell you:

  • Which changes will break your specific setup

  • How to handle conflicting dependencies

  • What order to apply changes in

  • How to debug when things inevitably break

The Manual, Step-by-Step Strategy

Instead of trying to implement all suggested changes at once, we adopted a methodical approach that proved far more effective:

1. Start with Package.json Updates

First, we manually updated the core dependencies in package.json:

{
 "dependencies": {
 "react": "18.2.0",
 "react-native": "0.81.0"
 }
}

Then we updated other libraries to their latest compatible versions. This gave us control over which packages to upgrade and when, rather than accepting all changes blindly.

2. Run and Break (Intentionally)

We ran the application knowing it would break. This was actually part of the plan. Each error became a checkpoint, a specific problem to solve rather than a mysterious failure buried in hundreds of changes.

Android: The Native Code Journey

Creating a Reference Project

One of our most effective strategies was creating a fresh React Native 0.81 project as a reference:

Key Files We Updated

settings.gradle The dependency resolution mechanism changed in 0.81. We compared our old file with the fresh project and updated:

dependencyResolutionManagement {
 repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
 repositories {
 google()
 mavenCentral()
 }
}

build.gradle Updated SDK versions and build tools:

buildscript {
 ext {
 buildToolsVersion = "33.0.0"
 minSdkVersion = 23
 compileSdkVersion = 34
 targetSdkVersion = 34
 }
}

MainActivity.java We compared our MainActivity with the reference project and added the new delegate pattern required for 0.81 compatibility.

We didn’t copy-paste blindly. Instead, we identified what was essential and what was specific to our project, merging changes thoughtfully.

Major Android Challenges

1. Animation Library Issues

The Reanimated library had breaking changes. We needed to upgrade to version 3.x and update our babel.config.js:

module.exports = {
 plugins: [
 'react-native-reanimated/plugin', // Must be listed last
 ],
};

2. Vision Camera (Face Detection)

This was our biggest challenge. We use Vision Camera for face detection functionality, and version 2.x didn’t support React Native 0.81.

The problem: Native module crashes with “TurboModule not found” errors.

The solution: Upgrade to Vision Camera 3.x, which required:

3. Deprecated Libraries

Some libraries simply weren’t maintained anymore. Here are the main ones we had to replace:

Old [email protected] on AndroidUpgrade to [email protected] changesUpgrade to 4.0+ with breaking [email protected] errorsUpgrade to 13.14.0

Each replacement required testing to ensure feature parity, especially for critical features.

iOS: The Conservative Approach

Podfile Updates

We updated the iOS platform version and enabled Hermes:

platform :ios, '13.4'

use_react_native!(
 :path => config[:reactNativePath],
 :hermes_enabled => true,
 :fabric_enabled => false # New Architecture disabled
)

Keeping AppDelegate in Objective-C

While React Native 0.81 encourages moving to Swift for the AppDelegate, we made a conscious decision to keep our existing Objective-C implementation.

Why?

  • Stability over novelty for production app

  • Existing push notification code worked reliably

  • Reduced testing surface area

  • Swift migration could be a separate, future initiative

We did need to update some initialization code in AppDelegate.m to match the new React Native patterns, but keeping it in Objective-C meant we could focus on React Native changes rather than also learning Swift migration patterns.

New Architecture: On the Roadmap

We’re currently running with the New Architecture disabled:

# android/gradle.properties
newArchEnabled=false

Why wait?

  1. About 30% of our dependencies didn’t have full New Architecture support yet

  2. Upgrading the version AND adopting New Architecture simultaneously felt too risky

  3. We wanted to stabilize on 0.81 first before the next major change

Our plan: Enable New Architecture in the next quarter after:

  • Verifying all libraries are compatible

  • Setting up comprehensive testing

  • Establishing performance baselines

The Philosophy: Small Steps, Steady Progress

Our Debugging Process

  1. Make one change (update a gradle file, fix MainActivity, etc.)

  2. Run the app and let it break

  3. Read the error carefully

  4. Compare with reference project to see what’s different

  5. Fix that specific issue

  6. Test again before moving to the next error

This “fix one thing at a time” approach meant we always knew what caused each issue. No mystery bugs from changing 50 things at once.

What Worked

Incremental fixes: Solving one breaking issue at a time made debugging manageable. When we changed 5 files and got an error, we knew exactly where to look.

Reference project: Having a fresh React Native 0.81 project to compare against was invaluable. Whenever we were stuck, we’d check “what does a clean 0.81 project do here?”

Test continuously: After each fix, we ran the app to verify nothing else broke. Better to catch issues immediately than after 10 more changes.

Document everything: We kept notes on every change and why we made it. This became our playbook for future upgrades and helped team members understand the changes.

What Didn’t Work

Upgrade Helper alone: For complex projects, automated suggestions need human judgment and understanding of your specific setup.

Big bang approach: Trying to fix everything at once led to confusion about what caused which issue. Too many variables.

Assuming backwards compatibility: Many libraries had breaking changes that required careful migration and testing.

Time Investment vs. Quality

Yes, this approach took longer than jumping to a “direct solution.”

But here’s why it was worth it:

  1. Zero production incidents: We caught every issue before it reached users

  2. Better understanding: The whole team learned what changed and why, not just one person

  3. Maintainable codebase: Our fixes were intentional and well-understood, not desperate patches

  4. Confidence: We could explain every change to stakeholders

  5. Reusable process: Next upgrade will be faster with this playbook

Key Takeaways

  1. Create a reference project: A fresh 0.81 install is your best documentation for native code changes

  2. Fix one thing at a time: Change one file, test, repeat. It takes longer but you stay in control

  3. Don’t adopt everything at once: New Architecture can wait. Focus on version stability first

  4. Replace deprecated libraries proactively: Check compatibility before you start, not when you hit errors

  5. Use native comparison: For native code (MainActivity, build.gradle, AppDelegate), side-by-side comparison with a fresh project reveals exactly what changed

With React Native 0.81 stable in our project, we’re now planning:

  • New Architecture adoption

  • Migrating iOS AppDelegate to Swift (when it makes sense)

  • Establishing this incremental approach as our standard for future upgrades

Join Govind on Peerlist!

Join amazing folks like Govind and thousands of other builders on Peerlist.

peerlist.io/

It’s available... this username is available! 😃

Claim your username before it's too late!

This username is already taken, you’re a little late.😐

0

0

0