
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.
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
Instead of trying to implement all suggested changes at once, we adopted a methodical approach that proved far more effective:
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.
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.
One of our most effective strategies was creating a fresh React Native 0.81 project as a reference:
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.
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:
Updating to [email protected]
Installing new peer dependency react-native-worklets-core
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.
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
)
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.
We’re currently running with the New Architecture disabled:
# android/gradle.properties
newArchEnabled=false
Why wait?
About 30% of our dependencies didn’t have full New Architecture support yet
Upgrading the version AND adopting New Architecture simultaneously felt too risky
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
Make one change (update a gradle file, fix MainActivity, etc.)
Run the app and let it break
Read the error carefully
Compare with reference project to see what’s different
Fix that specific issue
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.
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.
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.
Yes, this approach took longer than jumping to a “direct solution.”
But here’s why it was worth it:
Zero production incidents: We caught every issue before it reached users
Better understanding: The whole team learned what changed and why, not just one person
Maintainable codebase: Our fixes were intentional and well-understood, not desperate patches
Confidence: We could explain every change to stakeholders
Reusable process: Next upgrade will be faster with this playbook
Create a reference project: A fresh 0.81 install is your best documentation for native code changes
Fix one thing at a time: Change one file, test, repeat. It takes longer but you stay in control
Don’t adopt everything at once: New Architecture can wait. Focus on version stability first
Replace deprecated libraries proactively: Check compatibility before you start, not when you hit errors
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
0
0
0