
You upgrade to React Native 0.87, run a type check out of habit, and your terminal turns red in places it's never been red before. That's not a bug; it's the point. After roughly a year as an opt-in preview almost nobody enabled, the Strict TypeScript API is now simply the API, and every app that updates inherits the change whether it asked for it or not.
That's the headline, but it's far from the only thing that shipped this week. Metro got noticeably lighter, Swift Package Manager quietly arrived as an alternative to CocoaPods, and Android projects now have a supported path onto AGP 9. There's also a long tail of smaller changes sitting in the changelog that never made the official announcement but are worth knowing about.
Since React Native 0.80, there's been an opt-in flag called the Strict TypeScript API. It replaced the framework's hand-maintained .d.ts files — which had drifted out of sync with the actual implementation for years — with types generated straight from source. Almost nobody turned it on, because almost nobody has time to opt into a breaking change on a whim. 0.87 removes that choice. It's the default now.
The practical upside is real: types that actually match what the code does, doc comments that show up when you hover a component in your editor, and an API surface that's now explicitly defined as "whatever react-native exports at its root" — meaning an internal refactor inside Meta's codebase can no longer silently break your build. The practical downside is also real: if your app reaches into react-native/Libraries/* anywhere — and a lot of apps do, usually through a dependency rather than anything you wrote yourself — that's now a type error. Some type names changed too; refs, for instance, now resolve to dedicated types like ViewInstance and TextInputInstance instead of the old generic component instance types.
If you're not ready, there's an escape hatch, but treat it as a deadline rather than a permanent setting:
{
"extends": "@react-native/typescript-config",
"compilerOptions": {
"customConditions": ["react-native", "react-native-legacy-deep-imports"]
}
}
That flag is only supported through 0.88. After that, the legacy types go away entirely. There's an ESLint auto-fixer for the common cases, and if you'd rather hand the migration to a coding agent, there's already a purpose-built skill (migrate-to-strict-api) written for exactly that.
Metro jumps from 0.84 to 0.87 in lockstep with the framework, and it's a genuinely nice upgrade rather than a routine version bump. Source map generation is roughly twice as fast, which you'll feel every time React Native DevTools loads. Source map storage got more efficient too, cutting Metro's memory footprint roughly in half — a bigger deal than it sounds on large monorepos where the bundler process has historically been the thing that makes your laptop fans spin up.
TypeScript and ESM config files (metro.config.mts and friends) are now officially stable rather than experimental, and Metro can resolve a package against itself, which is a small but overdue fix for anyone who's had to work around self-imports. In exchange, support for *.es6 *config files and YAML configs is gone — if you're one of the handful of people still using either, this is your two-line migration.
React Native 0.87 adds experimental support for Swift Package Manager as an alternative to CocoaPods on iOS. It's opt-in, it's additive, and CocoaPods is still the default and the only officially supported path — but the fact that it exists at all, reusing the same prebuilt XCFrameworks React Native already ships, is a big deal for anyone who's ever lost an afternoon to a pod install failure.
Setup doesn't touch Ruby, Bundler, or CocoaPods at all:
cd ios npx react-native spm -- deintegrate
That command rewires your existing .xcodeproj to use Swift package references instead of replacing the project outright, so your signing and build phases stay exactly as they were. npx react-native spm deinit undoes it cleanly if you change your mind. The nicest part of the workflow: you only run the setup command once. After that, installing or removing a native dependency and rebuilding is enough — autolinking re-runs on its own, no pod install-equivalent step to remember.
There are real limitations right now. A community library needs to ship a Package.swift, and if it doesn't, you'll need to generate one yourself with npx react-native spm scaffold. CI and fresh clones need one npx react-native spm run before the first build. And the team is explicit that the commands and generated layout may still change — this is not something to point a production app at yet.
Getting there required some restructuring of how React Native ships its own precompiled binaries, since SwiftPM is far stricter than CocoaPods about XCFramework structure. Two new headers-only frameworks — ReactNativeHeaders.xcframework and ReactNativeDependenciesHeaders.xcframework — now exist so that every header has exactly one physical home. The one thing that'll actually bite you: bare-form angle imports without a namespace now need one.
- #import <RCTAppDelegate.h> + #import <React/RCTAppDelegate.h>
Header contents themselves are byte-identical to what CocoaPods ships, so this is a find-and-replace, not a rewrite.
0.87 is the first release to support Android Gradle Plugin 9, which brings its own set of breaking changes independent of anything React Native does. The team's guidance is to opt out of AGP 9's built-in Kotlin handling and its new DSL behavior for now, by adding two lines to android/gradle.properties:
android.builtInKotlin=false android.newDsl=false
Those opt-outs go away starting with AGP 10, so this is a temporary landing pad, not a permanent config. The Upgrade Helper already reflects this if you're using it.
The official release notes cover the four items above and stop there. Digging through the actual commit history turns up a handful of smaller changes that are worth knowing about even though nobody wrote a paragraph for them.
Styling libraries just got easier to build. Several component prop types moved from type aliases to interface declarations. That sounds like a nitpick, but it's exactly what TypeScript needs to support declaration merging — which is how libraries like Uniwind and NativeWind bolt extra props (like className) onto core RN components without resorting to as any casts or maintaining their own shadow type definitions. If you maintain a styling or theming library, this quietly removes a category of hack you've probably been carrying around.
window** and navigator are locked down.** Both globals are now non-writable and non-configurable, matching how real browsers behave. It's a small hardening change, but it closes off a genuinely annoying class of bug where some dependency deep in your tree silently reassigns a global and you spend an hour figuring out why navigator.onLine suddenly doesn't exist anymore.
A quiet performance fix in StyleSheet. Flattening nested style arrays no longer allocates a pile of throwaway intermediate objects along the way. It's not going to show up on a benchmark chart, but on list-heavy screens with a lot of conditional styling, this is the kind of fix that adds up.
The fake version number is finally gone. If you've ever cracked open a crash report from a prebuilt iOS binary and seen it self-report as version 1000.0.0 — a dev placeholder that had a habit of showing up in the worst possible stack traces — that's fixed. Prebuilt Core artifacts now stamp in the actual release version.
IntersectionObserver inches forward. IntersectionObserverEntry is now exposed as a global, one more piece of the still-Canary-gated Web Performance API work landing in the tree.
Legacy Architecture keeps getting sanded down. More deprecated protocol APIs and unused setup utilities were deleted this cycle. Nothing user-facing, but it's a reminder the old architecture is being dismantled piece by piece rather than removed in one dramatic release.
DevTools got minor polish — new options in the app menu, plus an updated app icon for macOS 26/27 — small enough to not warrant its own headline, but a sign the DevTools team is still actively shipping.
Taken together, this reads less like a features release and more like a cleanup release: more things removed and tightened than added. That's not a complaint. For a framework whose maintainers keep talking about eventually reaching a stable 1.0, this is exactly the unglamorous work that has to happen first.
For the skimmers, here's the full breaking-change list in one spot:
Node.js 22.13.0+ is now required.
Android: Kotlin 2.0+ (bundled version is 2.2.0), minCompileSdk raised to 34, compileSdk/buildTools bumped to 37.
Deep imports into Libraries/* and src/private/ are gone unless you opt out (see above).
Deprecated *Properties type aliases (like ViewProperties) are inaccessible under the Strict API — use the *Props equivalents.
YAML Metro configs and .es6 config files no longer work.
InteractionManager is removed — use requestIdleCallback.
The deprecated Modal animated prop is gone.
Deprecated StatusBar props (backgroundColor, translucent, networkActivityIndicatorVisible) and their setters are removed.
ScrollView's keyboardShouldPersistTaps no longer accepts a boolean.
The useTurboModules feature flag is gone — TurboModules are simply always on now.
useColorScheme() returns ColorSchemeName | null and will never return *unspecified *again.
NativeDialogManagerAndroid and the undocumented Touchable root export are gone (extend ViewProps instead).
NativeMethods and NativeMethodsMixin types are removed — use HostInstance.
A few packages also disappeared or changed shape: @react-native/core-cli-utils is no longer published, rn-get-polyfills is replaced by @react-native/js-polyfills, @react-native/jest-preset must be installed as a real package dependency now, and you can no longer connect the standalone react-devtools package over WebSocket — React Native DevTools is the only supported debugger now.
On the deprecation list for a future removal: InitializeCore (moving to react-native/setup-env), @react-native/assets-registry, ImageBackground (replace with a View plus an absolutely positioned Image), and on Android specifically, DrawerLayoutAndroid and the old UIBlock APIs.
Depends what you're maintaining. If you're starting something new, or you've already been running the Strict API preview since 0.80, this should be close to a non-event — most of the sharp edges were sanded off during the year-long opt-in period. If you're sitting on a large, multi-year-old codebase with deep imports scattered through it (often via a dependency you don't control rather than your own code), budget real time — a day is a reasonable guess, more if your import graph is creative. Either way, the legacy-types opt-out gives you one more release to plan around before it's gone for good.
0.87 also marks the end of the line for 0.84.x support, so if you're two versions back, this is a good moment to stop putting off the upgrade train rather than let it get longer.
To create a fresh project with 0.87, just run:
npx @react-native-community/cli@latest init MyProject --version latest
💡 Tip: Depending on your preferred package manager, you can easily swap out
npxforbunx,yarn dlx(Yarn v2+), orpnpm dlxto initialize your project.
(If you are an Expo user, React Native 0.87 is currently available as part of the expo@canary releases!)
As always, check out the React Native Upgrade Helper to diff your specific version, and read through the full changelog on GitHub for the nitty-gritty details.

React Native 0.86 lands with zero user-facing breaking changes, full Android 15 edge-to-edge support, powerful new DevTools tracing, and a brand new home.

Welcome to the Mhpdev blog! Join us for engineering deep dives, open-source updates, and practical solutions for building high-performance software.