text
$ npx react-native run-android
info Starting JS server…
info Building and installing the app on the device (failed reverse-proxy)…
Task :app:installDebug FAILED
FAILURE: Build failed with an exception.
- What went wrong:
Execution failed for task ‘:app:mergeLibDexDebug’.A failure occurred while executing com.android.build.gradle.internal.tasks.CheckDuplicatesRunnable
Duplicate class com.facebook.jni.HybridData found in modules jetified-fbjni-0.5.1 (com.facebook.fbjni:fbjni:0.5.1) and jetified-hermes-android-0.74.2-debug (com.facebook.react:hermes-android:0.74.2)
Go to the documentation to learn how to Fix duplicate class errors:
https://developer.android.com/r/tools/gradle-multi-dex-error
- Try:
Run with –stacktrace option to get the stack trace. Run with –info or –debug option to get more log output. Run with –scan to get full insights.
BUILD FAILED in 4m 12s
I remember when we thought the Bridge was the solution to all our problems. We were wrong.
Back in the PhoneGap days, we were just happy to see a `console.log` show up in a Safari web inspector connected to a physical iPhone 4. We thought we were geniuses for wrapping a slow-as-molasses WebView in a native container and calling it an "app." Then **react native** showed up, promising us the "native" feel without the "native" effort. They told us we could write JavaScript and get a real UI. They didn't tell us we’d spend 40% of our lives fighting with Ruby versions and another 40% watching Gradle download the entire internet just to tell us that two versions of a C++ library are fighting for dominance in a `dex` file.
Sit down. Grab a drink. If you’re going to survive a **react native** 0.74 migration, or even just a Tuesday morning build, you need to stop thinking like a web developer and start thinking like a demolition expert. One wrong move in your `node_modules`, and the whole thing comes down.
## 1. The Dependency Hellscape: Why your `package.json` is a ticking time bomb.
In the web world, a minor version mismatch in a dependency usually just means a slightly larger bundle or a missing CSS feature. In **react native**, a minor version mismatch is a death sentence. You’re not just managing JavaScript; you’re managing a fragile ecosystem of Java, Kotlin, Objective-C, C++, and Swift.
The `package.json` is a lie. It’s a list of suggestions, not a list of reality. When you see `^0.73.0`, you’re inviting the chaos of the universe into your project. I’ve seen teams lose an entire sprint because a transitive dependency—something three levels deep that they didn't even know they were using—decided to update its `android/build.gradle` to use a newer version of the Android Gradle Plugin (AGP) that wasn't compatible with the current version of **react native**.
Here is a war story for you. We had a production app for a major logistics firm. We added a simple library for SVG support. Everything worked on the dev machines. We pushed to the CI/CD pipeline. The build failed. Why? Because the SVG library pulled in a version of `react-native-render-html` that had a peer dependency conflict with the core. The autolinking script tried to link a library that didn't exist in the iOS workspace, leading to a `Linker Command Failed with Exit Code 1`.
If you want to survive, you treat `yarn.lock` or `package-lock.json` like a holy relic. You don't just `npm install`. You audit. You use `npx react-native-clean-project` like a ritualistic cleansing. And for the love of all that is holy, you check your `node_modules` for duplicate versions of `fbjni`. If you see two versions of that library, your Android build is already dead; it just hasn't stopped breathing yet.
## 2. The JSI and the Ghost of the Bridge: A Post-Mortem.
For years, we blamed "The Bridge." The Bridge was that narrow, asynchronous pipe where we serialized everything into JSON, sent it to the native side, and waited for a response. It was like trying to build a skyscraper by sending instructions through a carrier pigeon.
Now, we have the "New Architecture." Fabric and TurboModules. They gave us the JSI (JavaScript Interface). No more JSON serialization. Now, JavaScript can hold a reference to a C++ HostObject. It sounds like heaven, doesn't it? It’s not. It’s just a different kind of hell.
The New Architecture requires you to write `codegen`. You have to define your specs in TypeScript or Flow, and then **react native** generates the C++ glue code. If you’ve never had to debug a C++ stack trace inside a JavaScript environment, you haven't lived. You’ll be looking at `AppDelegate.mm` or a `TurboModule` implementation, and suddenly you’re dealing with memory management and pointer arithmetic.
```cpp
// This is the "future" they promised.
// A snippet of a TurboModule C++ implementation.
// Hope you remember your pointers, kid.
#include "NativeSampleModule.h"
namespace facebook::react {
NativeSampleModule::NativeSampleModule(std::shared_ptr<CallInvoker> jsInvoker)
: NativeSampleModuleSpecJSI(jsInvoker) {}
std::string NativeSampleModule::getString(jsi::Runtime& rt, jsi::String stringArgument) {
return stringArgument.utf8(rt);
}
// If this fails, the app doesn't just 'red box'.
// It crashes the entire OS process with a SIGSEGV.
} // namespace facebook::react
The “Ghost of the Bridge” still haunts us because most of the ecosystem hasn’t fully migrated. You’ll find yourself in a “hybrid” state where half your libraries use the old bridge and half use TurboModules. This “interop layer” is where performance goes to die. You’re paying the memory overhead for both systems. It’s like trying to run a marathon while carrying a bicycle on your back because you might hit a paved road eventually.
Table of Contents
3. Android Gradle Files: The Place Where Joy Goes to Die.
If you spend enough time in react native, you start to recognize the specific smell of a failing Gradle build. It smells like ozone and wasted billable hours. The android/build.gradle and android/app/build.gradle files are the most dangerous files in your repository.
The problem is that react native is a moving target. Version 0.73 moved to Java 17. Version 0.74 changed how the hermes-engine is consumed. If you’re upgrading, you can’t just change the version number in package.json. You have to go into the guts of the Android folder and perform surgery.
Take ProGuard, for example. You want to shrink your APK? Great. Now your app crashes only in production because ProGuard decided that the C++ method names used by Hermes weren’t “important” and stripped them out. You’ll spend three days staring at a Mapping.txt file trying to figure out why com.facebook.react.bridge.NativeModule disappeared.
// The ProGuard/R8 rules you'll eventually copy-paste from a
// 4-year-old StackOverflow post just to get the app to launch.
-keep class com.facebook.hermes.unicode.** { *; }
-keep class com.facebook.jni.** { *; }
-keepattributes Signature, InnerClasses, EnclosingMethod
-dontwarn com.facebook.react.**
// And then you hit the 64k method limit because
// your dependencies are bloated.
android {
defaultConfig {
multiDexEnabled true
}
}
And don’t get me started on the ndk.abiFilters. You’ll build an APK that works on your Pixel, but the moment it hits a user with an older ARMv7 device, it’s an instant crash because you forgot to include the right .so files. You’re not a developer; you’re a configuration manager who occasionally writes a View.
4. iOS CocoaPods and the Infinite Rebuild Loop.
On the iOS side, we have CocoaPods. It’s a dependency manager written in Ruby, which is the first sign of trouble. To run a react native app on iOS, you need the right version of Node, the right version of Ruby, the right version of Xcode, and the right version of the Command Line Tools. If any of these are off by a patch version, pod install will fail with a cryptic error about ffi or deployment targets.
The M1/M2 Mac transition was a bloodbath for react native developers. We spent a year prefixing every command with arch -x86_64. We were fighting the hardware, the OS, and the framework all at once. Even now, with 0.74, you’ll find yourself stuck in an infinite rebuild loop. You change one line in AppDelegate.mm to initialize a library, and Xcode decides it needs to recompile 400 files, including the entire Yoga layout engine.
// ios/YourProject/AppDelegate.mm
// The "New Architecture" entry point.
// One missing bracket here and Xcode will give you 200 unrelated errors.
#import "AppDelegate.h"
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <React/RCTAppSetupUtils.h>
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.moduleName = @"YourProject";
// You used to just init the bridge. Now you have to manage the
// Fabric TurboModule delegate and the concurrent root.
self.initialProps = @{};
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
The real pain is the Podfile.lock. You’ll have a developer on a Mac with Xcode 15.3 and another on 15.0. Their Podfile.lock will keep fighting each other, changing checksums every time someone runs pod install. You’ll end up with merge conflicts in a 5,000-line generated file. My advice? If the pods are working, don’t touch them. Don’t even look at them. If you breathe too hard on the ios folder, you’ll be spending your afternoon deleting DerivedData and praying to the ghost of Steve Jobs.
5. State Management Overkill: Why Redux is not a personality trait.
Somewhere along the line, people decided that a simple Todo list app needed a “Single Source of Truth” with actions, reducers, sagas, and selectors. I’ve walked into projects where it takes 15 files to update a boolean.
In react native, state management isn’t just about data flow; it’s about performance. Every time your “Global Store” updates, you risk triggering a re-render of the entire component tree. On a web browser, you might not notice. On a mid-range Android phone from 2021, you’ll see the frame rate drop to single digits.
People treat Redux like a personality trait. They refuse to use local state even for a text input. So, every keystroke travels from the native text input, across the bridge (or JSI), into the Redux store, through a series of reducers, and back down to the component. It’s a 300ms round trip for a single character. The user is typing faster than the state can keep up, and suddenly the cursor is jumping around like it’s possessed.
Then came the “Context API” era, where everyone thought they could replace Redux. They just ended up creating “Provider Hell,” where changing the user’s theme color causes the entire navigation stack to unmount and remount.
If you want to survive, use Zustand or signals. Or better yet, use local state. Not everything needs to be global. Your “User Profile” doesn’t need to know that the “Settings Toggle” on page 4 was clicked. Stop over-engineering your data and start worrying about your memory footprint. Hermes is good, but it’s not a miracle worker. If you leak memory in your state, Hermes will eventually just give up and let the OS kill your app.
6. Performance Tuning: When the Frame Rate Drops to Zero.
This is where the men are separated from the boys. In react native, you have two main threads: the UI thread (Main thread) and the JS thread. If the JS thread is busy calculating the Fibonacci sequence or parsing a 5MB JSON response, your UI thread is stuck. The user taps a button, and… nothing. The app is frozen.
The Yoga layout engine is what calculates the positions of your views. It’s a C++ implementation of Flexbox. It’s fast, but it’s not free. If you have a deeply nested view hierarchy, Yoga has to do a lot of math. If you’re doing that inside a FlatList with 500 items, you’re going to see the “White Screen of Death” while scrolling.
// The classic "Why is my app slow?" snippet.
// Using an anonymous function in a render item.
// Every scroll tick creates a new function, triggering a re-render.
<FlatList
data={hugeArray}
renderItem={({ item }) => (
<TouchableOpacity onPress={() => handlePress(item.id)}>
<Text>{item.title}</Text>
</TouchableOpacity>
)}
// If you don't use getItemLayout,
// Yoga has to measure every item on the fly.
/>
I once worked on an app that had a “smooth” 60fps on the iPhone 15 Pro, but on a Samsung A12, it was a slideshow. We found out the developer was using onLayout to calculate the height of every element to manually position a floating action button. Every time the layout changed, it triggered a state update, which triggered a re-render, which triggered another onLayout. It was a feedback loop that consumed 100% of the CPU.
You have to learn to use the Profiler. You have to understand why useMemo and useCallback exist—not because they’re “cool,” but because they are the only things keeping your app from collapsing under its own weight. You have to look at the “Shadow Tree” and realize that every <View> you add is a memory allocation on the native side.
And for the love of everything, use FlashList from Shopify instead of the standard FlatList. The standard FlatList doesn’t actually recycle views; it just unmounts them and mounts new ones. It’s a garbage collection nightmare. FlashList actually does what a native list does: it reuses the underlying native views. It’s the difference between a professional tool and a toy.
The Final Debrief
You’re still here? Good. You might just have the stomach for this.
react native is a powerful tool, but it’s a deceptive one. It lures you in with the promise of “Learn Once, Write Anywhere,” but the reality is “Learn Everything, Debug Everywhere.” You’ll spend more time in Xcode and Android Studio than you will in VS Code if you’re doing anything meaningful.
If you want a “seamless” experience (wait, I’m not allowed to say that), if you want a smooth ride, go native. Write Swift. Write Kotlin. But if you’re stuck in the trenches with us, if you need to ship one codebase to two platforms on a startup budget, then welcome to the war.
Just remember:
1. Lock your versions.
2. Respect the JSI but fear the C++.
3. Keep your Gradle files clean and your ProGuard rules tighter.
4. Don’t let CocoaPods smell your fear.
5. Keep your state local and your lists recycled.
Now, get out of here. I hear a yarn install calling your name, and you’ve got about 4,000 peer dependency warnings to ignore before the sun comes up. Just don’t come crying to me when your AppDelegate.mm won’t compile because you forgot to add the RCTAppDelegate header. You’re on your own now.
And hey… if you see a “Bridge is disconnected” error? Just restart the Metro bundler. It’s the only “magic” we have left.
Related Articles
Explore more insights and best practices: