text
[!] CocoaPods could not find compatible versions for pod “React-Core”:
In Podfile:
React-Core (from ../node_modules/react-native/React)
Specs satisfying the React-Core (from../node_modules/react-native/React) dependency were found, but they required a higher minimum deployment target.
/Library/Ruby/Gems/2.6.0/gems/cocoapods-1.15.2/lib/cocoapods/resolver.rb:406:in resolve'analyze’
/Library/Ruby/Gems/2.6.0/gems/cocoapods-1.15.2/lib/cocoapods/installer/analyzer.rb:1078:in
/Library/Ruby/Gems/2.6.0/gems/cocoapods-1.15.2/lib/cocoapods/installer.rb:214:in resolve_dependencies'install!’
/Library/Ruby/Gems/2.6.0/gems/cocoapods-1.15.2/lib/cocoapods/installer.rb:161:in
— Failed to build iOS project.
Error: Could not find iPhone 15 simulator.
CompileC /Users/admin/Library/Developer/Xcode/DerivedData/Project-abc/Build/Intermediates.noindex/Pods.build/Debug-iphonesimulator/React-Core.build/Objects-normal/arm64/RCTBridge.o …
In file included from /node_modules/react-native/React/Base/RCTBridge.m:10:
/node_modules/react-native/React/Base/RCTBridge.h:12:9: fatal error: ‘React/RCTBridgeDelegate.h’ file not found
Table of Contents
import
^~~~~~~~~~~~~~~~~~~~~~~~~~~
1 error generated.
** BUILD FAILED **
## The Versioning Trap: Why 0.75 Isn't Just a Number
We started this migration thinking 0.74.x to 0.75 was a minor bump. We were wrong. In the world of react native, a minor version increment is often a euphemism for "we rewrote the underlying C++ core and forgot to tell the community about the breaking changes in the JSI layer." We were moving from 0.68, a version that still clung to the old bridge like a security blanket, straight into the maw of the New Architecture.
The first thing that hits you isn't the code; it's the environment. You realize that your Ruby version for CocoaPods is suddenly incompatible with the new Gemfile requirements. You realize your Node version is too old for the new Metro transformer. You spend four hours just getting `yarn install` to finish without a peer dependency conflict that locks your terminal in a recursive loop of misery.
When you finally get the dependencies to settle, you open `AppDelegate.mm`. The old `RCTBridge` is gone, replaced by `RCTAppDelegate`. This isn't just a rename. It’s a fundamental shift in how the react native runtime initializes. You’re no longer just passing a URL to a bundle; you’re managing the `fabricEnabled` and `turboModulesEnabled` flags, which, if misconfigured, lead to a white screen of death with zero console output. We spent twelve hours chasing a null pointer in `RCTSurfacePresenter` because a third-party library was still trying to hook into the old `RCTBridge` instance that no longer existed.
The `package.json` becomes a graveyard of "resolutions." You find yourself forcing `react-is` to version 18.0.0 because three different libraries are fighting over it, and if you don't, the Metro bundler decides to throw a `TransformError` that points to a line of code that doesn't exist in your project.
## CocoaPods and the Art of Silent Failures
If there is a hell, it is paved with `Podfile.lock` files. Moving to react native 0.74+ requires a total re-evaluation of how you handle static linking. We had `use_frameworks! :linkage => :static` in our Podfile to support some legacy Swift dependencies. The moment we flipped the New Architecture switch, the linker exploded.
```text
ld: warning: directory not found for option '-L/Users/admin/Library/Developer/Xcode/DerivedData/Project-abc/Build/Products/Debug-iphonesimulator/DoubleConversion'
ld: warning: directory not found for option '-L/Users/admin/Library/Developer/Xcode/DerivedData/Project-abc/Build/Products/Debug-iphonesimulator/fmt'
ld: library not found for -lfolly_runtime
clang: error: linker command failed with exit code 1 (use -v to see invocation)
--- Checking Podfile.lock checksums...
--- Podfile.lock checksum mismatch. Please run `pod install`.
--- (Run this 50 more times, it won't help).
The issue is the way react native now handles its internal C++ dependencies. Folly, glog, and DoubleConversion are no longer just background players; they are the backbone of the JSI. If your Header Search Paths are off by even a single relative directory, the compiler will tell you it can’t find RCTBridgeDelegate.h, even though you can see it right there in the project navigator.
We had to manually patch the Podfile to exclude certain architectures. We had to strip out the Flipper integration because, let’s be honest, Flipper has been broken since 0.69 and only serves to add 4 minutes to your build time and 20MB to your binary. Removing Flipper in 0.74 isn’t just a suggestion; it’s a requirement for sanity. But even then, the post_install hooks in the Podfile are a minefield. You’re manually editing IPHONEOS_DEPLOYMENT_TARGET for fifty different pods because one library decided it still needs to support iOS 11, while the react native core now demands iOS 13.4 at a minimum.
Then there’s the Hermes engine. In the new versions, Hermes is the default, but the way it links via CocoaPods is fragile. If you have a custom build_configuration, Hermes might fail to copy its framework into the app bundle, leading to a crash on launch that only happens on physical devices, never on the simulator.
The JSI Myth vs. The Reality of the Bridge
The marketing for the New Architecture talks about the “death of the bridge” and the “seamless” (a word I hate) transition to JSI (JavaScript Interface). The reality is that you are now writing more C++ than you ever wanted to. If you have a custom native module, you can’t just export it with RCT_EXPORT_MODULE() and call it a day. You now have to deal with Codegen.
Codegen is a black box that takes your TypeScript specs and generates C++ boilerplate. When it works, it’s fine. When it fails, it provides errors like Command PhaseScriptExecution failed with a nonzero exit code. No explanation. No stack trace. Just a failure. You have to go into the node_modules/react-native/scripts directory and manually run the shell scripts to see that it failed because you used a type alias instead of an interface in your spec file.
The JSI is supposed to be faster because it eliminates JSON serialization. And it is. But the overhead of managing the C++ state across the boundary is a nightmare. We found that our TurboModules were leaking memory because the jsi::Runtime wasn’t correctly cleaning up host objects when the component unmounted. We were seeing a 200MB heap growth over a ten-minute session.
# Fatal error in ../../v8/src/api/api.cc, line 1122
# Check failed: !value_obj->IsJSReceiver() || value_obj->IsJSProxy() || (value_obj->GetCreationContext().ToLocal(&context) && context->GetIsolate() == isolate).
# Failure Message: [react-native] JSI HostObject dropped on wrong thread.
1: 0x1045a2344 - RCTJSIExecutor::callFunction
2: 0x1045a2890 - facebook::jsi::Runtime::createHostObject
3: 0x1045b1212 - std::__1::__function::__func<...>::operator()
This error haunted us for 48 hours. It turns out that if you call a JSI-bound function from a background thread in Objective-C without explicitly dispatching to the JS thread, the entire runtime just gives up. The “New Architecture” doesn’t protect you from thread safety; it just makes the crashes harder to debug because they happen in the C++ layer instead of the JS layer.
Android’s Gradle Hell: A Case Study in Dependency Conflict
While iOS was failing to link, Android was failing to even sync. The jump to react native 0.74 requires moving to Gradle 8.x and Android Gradle Plugin (AGP) 8.1+. This move deprecates the compile configuration (which should have been gone years ago but lingered in legacy libraries) and changes how namespaces are handled in build.gradle.
Every single one of our third-party libraries broke. We had to use patch-package on fourteen different repositories just to add a namespace declaration to their build.gradle files. Without this, AGP 8.x refuses to build the AAR.
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':react-native-reanimated:compileDebugJavaWithJavac'.
> Could not resolve all files for configuration ':react-native-reanimated:debugCompileClasspath'.
> Failed to transform react-native-0.74.2-debug.aar (com.facebook.react:react-android:0.74.2)
> Execution failed for JetifyTransform: /Users/admin/.gradle/caches/modules-2/files-2.1/com.facebook.react/react-android/0.74.2/react-android-0.74.2-debug.aar.
> Java heap space
The Jetifier is another ghost of the past that refuses to die. Even though we are well into the AndroidX era, some transitive dependencies still trigger the Jetify transform, which, in Gradle 8, is prone to out-of-memory errors. We had to increase the JVM heap size to 8GB just to compile the debug build.
Then there’s the ndkVersion. React native 0.74 requires a specific NDK version (usually 26.x). If you have another library that forces an older NDK, Gradle won’t tell you there’s a conflict. It will just pick one, and then your C++ compilation will fail with a cryptic error about __libcpp_verbose_abort not being defined. We spent six hours realizing that react-native-vision-camera was pulling in a different NDK toolchain than the core library.
And don’t get me started on the fbjni conflicts. If you have multiple libraries using different versions of Facebook’s JNI helper, the Android linker will just pick the first one it finds. If that version is incompatible with the react native version, you get a UnsatisfiedLinkError at runtime. Not at compile time. At runtime, usually five seconds after the app starts.
Hermes Memory Management: Where Your Performance Goes to Die
We were told Hermes would solve our performance woes. “Bytecode pre-compilation,” they said. “Reduced TTI,” they promised. What they didn’t mention is that the Hermes Garbage Collector (GC) is incredibly aggressive and, at the same time, surprisingly opaque.
In react native 0.74, the way Hermes interacts with the New Architecture’s Fabric renderer creates a massive memory overhead. Every shadow node in Fabric is a C++ object that needs to be mirrored in the JS heap. If you have a long list (even with FlashList), the coordination between the Hermes GC and the native side’s memory management starts to lag.
We observed a pattern where the HermesRuntime would refuse to release memory back to the OS even after a full GC cycle. We were looking at the adb shell dumpsys meminfo and seeing the “Private Dirty” memory climbing steadily.
** MEMINFO at pid 12345 [com.project.abc] **
Pss Private Private SwapPss Heap Heap Heap
Total Dirty Clean Dirty Size Alloc Free
------ ------ ------ ------ ------ ------ ------
Native Heap 85420 85400 0 24 124512 98422 26089
Dalvik Heap 12432 12400 0 8 24512 12432 12080
Stack 1432 1432 0 0
Cursor 24 24 0 0
Ashmem 128 128 0 0
Other dev 44 0 44 0
.so mmap 24512 2312 18400 0
.jar mmap 8432 0 4212 0
.apk mmap 12456 0 8400 0
.ttf mmap 124 0 80 0
.dex mmap 32456 16432 16024 0
.oat mmap 2132 0 1200 0
Unknown 45612 45600 0 0
The “Unknown” section is where the JSI host objects live. When you’re pushing the New Architecture to its limit, this section balloons. We found that the only way to mitigate this was to manually trigger a GC by calling global.gc() in our navigation hooks—a practice that is widely considered a hack but is practically mandatory if you want your app to survive on a device with 4GB of RAM.
Furthermore, the Hermes debugger is still lightyears behind the V8 inspector. Trying to profile a memory leak in Hermes is like trying to perform surgery with a spoon. You get a heap snapshot that is barely readable and often misses the connection between the JS object and the underlying C++ shadow node that is actually holding the memory.
Why ‘Write Once, Run Anywhere’ is a Lie We Tell Stakeholders
The project nearly collapsed because we treated react native as a cross-platform framework. It isn’t. It’s a way to orchestrate two separate, increasingly complex native projects using a shared JavaScript runtime. The “Write Once” dream dies the moment you have to implement a custom header or a complex gesture.
In the New Architecture, the divergence between iOS and Android is wider than ever. On iOS, you’re dealing with the MainQueue and TurboModule synchronization. On Android, you’re fighting with the Choreographer and the UI Thread. We had a bug where a simple animation would stutter on high-end Android devices but run fine on an iPhone 11. The cause? The way react-native-reanimated v3 interacts with the Fabric layout engine differs fundamentally between the two platforms.
On Android, the layout pass is synchronous within the onLayout call of the ReactViewGroup. On iOS, it’s handled via layoutSubviews in the RCTView. When you add the New Architecture’s “concurrent rendering” into the mix, you end up with race conditions where the JS state is updated, but the native shadow tree hasn’t committed the changes yet. This results in “flickering” components that stakeholders love to point out during demo calls.
We spent the last 12 hours of our 72-hour marathon debugging a ClassCastException in com.facebook.react.views.view.ReactViewManager. It only happened when a user navigated away from a screen while a modal was closing.
java.lang.ClassCastException: com.facebook.react.uimanager.LayoutShadowNode cannot be cast to com.facebook.react.fabric.events.EventBeat$EventBeatCallback
at com.facebook.react.fabric.Binding.stopSurface(Native Method)
at com.facebook.react.fabric.FabricUIManager.stopSurface(FabricUIManager.java:452)
at com.facebook.react.ReactRootView.unmountReactApplication(ReactRootView.java:582)
at com.swmansion.rnscreens.ScreenView.onDetachedFromWindow(ScreenView.kt:124)
This is the “ugly truth.” You aren’t just a JavaScript developer. You are a C++ developer, a Kotlin developer, an Objective-C developer, and a build engineer. If you aren’t prepared to dive into the node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/Binding.java file to understand why a surface is failing to unmount, you have no business using the New Architecture in production.
The migration is “finished” now, in the sense that the app builds and doesn’t crash in the first five minutes. But the “scars” are there. The Podfile is a mess of overrides, the build.gradle is full of exclude rules, and the AppDelegate.mm looks like a Frankenstein’s monster of legacy bridge code and new TurboModule initializers.
The next developer who inherits this codebase will look at the yarn.lock and wonder why we pinned react-native-screens to a specific commit hash. They’ll wonder why there’s a random Thread.sleep(16) in the native module that handles image processing. They won’t know about the 72 hours of hell. They’ll just see a “complex” project and probably suggest we “rewrite it in Flutter.”
Good luck to them. Flutter has its own graveyard. For now, I’m going to sleep, assuming the Metro bundler doesn’t find a way to crash my dreams with a Request to terminate took too long error. We still haven’t figured out why the sourcemaps are misaligned in Sentry for the production build, but that’s a problem for hour 73. I’m done. Don’t upgrade to 0.75 unless you’re prepared to lose your weekend and your sanity. The bridge might be dying, but the fire it left behind is still burning.
Related Articles
Explore more insights and best practices: