React Native 0.87 shipped on August 11, 2026, and made default one of the ecosystem's biggest type-system changes: the Strict TypeScript API. In this article we walk through the React Native 0.87 upgrade guide step by step — what breaks, which toolchain versions are now mandatory, and how to migrate your existing codebase safely.
💡 Pro Tip: Before you start the upgrade, add the"react-native-legacy-deep-imports"condition to thecompilerOptions.customConditionsarray in yourtsconfig.json. This is not an npm package — it's the official opt-out switch. It temporarily keeps the deep imports broken by the strict API working, letting you migrate section by section.
Table of Contents
- What 0.87 breaks
- Removed APIs and the iOS header change
- Strict TypeScript API: from deep imports to root exports
- What source-generated types gain — and what they break
- The Node 22 + AGP 9 + Kotlin 2.0 toolchain upgrade
- Metro 0.87 and cache issues
- Experimental SwiftPM on iOS: should you try it
- Step by step with Upgrade Helper
- Post-upgrade regression testing
- A shortcut if you're coming from 0.85/0.86
- FAQ
- What breaking changes are there in React Native 0.87?
- Do I have to enable the legacy deep-imports opt-out?
- What are the minimum Node and Gradle versions for RN 0.87?
- Is Swift Package Manager support usable in React Native?
- Should I move to 0.88 or to 0.87?
- If I'm using Expo, can I move RN directly to 0.87?
- Conclusion
- Sources
What 0.87 breaks
The React Native team describes 0.87 as an "ecosystem-wide" breaking change. The official announcement puts it this way: "This is an ecosystem-wide change and brings intentional breaking changes across the API surface." That sentence sums up why this release gets so much attention: the change affects not just your own codebase, but the third-party libraries you depend on too.
Behind it is a concrete consequence: if your project — directly or through a dependency — reaches into files under react-native/Libraries/*, it won't be surprising to hit red lines on your first tsc run after npm install. This article shows you where to find those red lines, in what order to fix them, and how to sync your toolchain.
The table below summarizes the main areas that changed in 0.87:
Area | 0.86 and earlier | With 0.87 |
|---|---|---|
TypeScript API | Deep imports were allowed | Only root exports; deep imports are a type error |
Type source | Manually maintained .d.ts | Auto-generated from source code |
Toolchain | Node 20 was supported | Node.js 22.13+, AGP 9, Kotlin 2.0+ required |
Bundler | Metro 0.84 | Metro 0.87 |
The first row in this table is the one that will give you the most trouble: the strict TypeScript API. We cover it in detail in the section below.
Removed APIs and the iOS header change
The announcement's "API removals" section lists concrete removals that can break your build independently of the type system. Worth searching your codebase for these before upgrading:
Removed | Replacement |
|---|---|
InteractionManager | requestIdleCallback |
NativeMethods / NativeMethodsMixin types | HostInstance |
*Properties type aliases (e.g. ViewProperties) | *Props equivalents |
src/private/ deep imports | Root exports |
Modal animated prop | Removed, no replacement |
StatusBar backgroundColor / translucent / networkActivityIndicatorVisible | Removed, including setters |
ScrollView keyboardShouldPersistTaps boolean value | String values |
useTurboModules feature flag | TurboModules always on |
NativeDialogManagerAndroid export | Removed |
YAML Metro config and .es6-extension config files | metro.config.mts / .ts / ESM |
On top of this, useColorScheme() now returns ColorSchemeName | null and no longer produces 'unspecified' — if a switch handles that return value, add the null branch.
On iOS there's a breaking change that's a side effect of the SwiftPM work: files including React Native headers without a namespace, using a "bare-form angle include," no longer work that way — the announcement calls this the one consumer-facing change of the SwiftPM work, and the fix is to add the namespace. The announcement shows the fix directly:
1// Old (no namespace — no longer valid with 0.87)2#import <RCTAppDelegate.h>3 4// New5#import <React/RCTAppDelegate.h>On Android, compileSdk and buildTools were bumped to 37; for libraries, minCompileSdk is 34.
Strict TypeScript API: from deep imports to root exports
In version 0.80, the React Native team introduced the Strict TypeScript API as an opt-in preview. As of 0.87 it's the default. The official docs (reactnative.dev/docs/strict-typescript-api) summarize the change: React Native's public TypeScript surface is now limited to the root exports of the react-native package; deep imports into internal paths like react-native/Libraries/... now produce a type error.
In practice this means: if your codebase has a line like
1import RCTDeviceEventEmitter from "react-native/Libraries/EventEmitter/RCTDeviceEventEmitter";the TypeScript compiler will error on this line once you move to 0.87, because RCTDeviceEventEmitter is no longer in the root export list. The fix is to find the equivalent exported from the root package, or to temporarily enable the official opt-out switch.
Watch out for a common mistake: react-native-legacy-deep-imports is not an installable npm package — it's a custom condition value inside tsconfig.json. The official announcement says it directly: "To temporarily revert to the previous types, add the react-native-legacy-deep-imports custom condition to your tsconfig.json." That's all you need to do:
1{2 "extends": "@react-native/typescript-config",3 "compilerOptions": {4 "customConditions": ["react-native", "react-native-legacy-deep-imports"]5 }6}But don't treat this as a permanent fix: the announcement states that this opt-out is a "temporary bridge," that it will remain available throughout React Native 0.88, and that the legacy types are planned for removal in the following release. Also note the opt-out only affects the TypeScript analysis in your own project — apps and libraries migrate independently of each other.
Deep imports like this tend to accumulate in a project's older layers: a notification module, an old native-bridge wrapper, or a single import line that got added years ago as a "quick fix." Scanning them one by one also surfaces the codebase's hidden native dependencies.
What source-generated types gain — and what they break
The second part of the change is less visible but more fundamental: React Native's TypeScript definitions are now generated directly from the source code, instead of manually synced .d.ts files. The concrete benefit is that when a prop or method changes on the native side, the type definition updates automatically — previously, the type file could lag behind the real API for a long time.
The breaking side is this: generation from source also drops types that were previously working "quietly" but were never officially exported. If a library or your own code imports one of those types (for example, the internal prop type of a native module), the generator no longer produces it, and your build breaks.
Support for React Native 0.84 ended with 0.87: the announcement says "0.87 is now the latest stable version of React Native and 0.84.x moves to unsupported." That creates mandatory migration pressure for teams still on 0.84, and means code paths using deep imports need to be scanned.
You can use a simple scan command to find deep imports:
1grep -rn "react-native/Libraries" src/ --include="*.ts" --include="*.tsx"Notice this doesn't constrain the quote style: searching for from 'react-native/Libraries only finds single-quoted imports, and double-quoted files silently slip past the scan. This command lists every match with file and line number; go through each and find its root-export equivalent.
A manual scan isn't the only option: the announcement points to ready-made ESLint fixers and the `/migrate-to-strict-api` skill; the fix for every breaking change is also listed in the strict API migration guide.
The Node 22 + AGP 9 + Kotlin 2.0 toolchain upgrade
0.87's second big change is in the build toolchain. The React Native 0.87 announcement sets the minimum versions as follows:
- Node.js: 22.13 or higher is required. The package's
enginesfield on the npm registry is exactly^22.13.0 || ^24.3.0 || >= 26.0.0; on an older Node 22 patch version npm only prints an EBADENGINE warning by default, but install fails ifengine-strictis on. - Android Gradle Plugin (AGP): 9 or higher.
- Kotlin: 2.0 or higher — the Kotlin version bundled with 0.87 is 2.2.0.
- Android minCompileSdk: 34 (libraries should target compileSdk >= 34);
compileSdk/buildToolswere bumped to 37.
To pin the Node version in CI, copy React Native's own range verbatim into package.json and validate it in CI with --engine-strict — this prevents different packages in a monorepo from being built with different Node versions:
1{2 "engines": {3 "node": "^22.13.0 || ^24.3.0 || >= 26.0.0"4 }5}On Android, update the Gradle wrapper and Kotlin version together; since minimum Kotlin is 2.0, upgrading AGP alone isn't enough for a project still on 1.x. Update both lines in android/build.gradle:
1// android/build.gradle2classpath("com.android.tools.build:gradle:9.0.0")3classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.0")The K2 compiler bundled with Kotlin 2.0 has its own ecosystem-wide effects; see our separate Kotlin 2.1: K2 Compiler + Context Parameters article. Similarly, React Native's move to strict type checking points the same direction as the TypeScript 7 release debate: compilers and type systems are getting stricter, faster, and less concerned with backward compatibility.
On the AGP 9 side there's one concrete action item in the announcement that most upgrade guides skip: this release recommends opting out of AGP 9's own built-in Kotlin support and its new DSL API. The announcement tells you to add these two flags to android/gradle.properties, and the Upgrade Helper diff brings the same lines:
1# Opt out of the built-in Kotlin and new DSL behavior that ships with AGP 9.2# These opt-outs will be removed starting with AGP 10.x.3android.builtInKotlin=false4android.newDsl=falseAGP 9.0 is a major release that brings several API and breaking changes to Gradle builds, so you need to plan Kotlin and AGP together, not separately.
Toolchain upgrades carry their own risk in a multi-package monorepo. If you're on Expo, there's one clear fact to know: the announcement says, "For Expo projects, React Native 0.87 will be available as part of the expo@canary releases" — there is no stable Expo SDK carrying 0.87. An issue in Expensify's own App repo (github.com/Expensify/App/issues/101427) confirms this: Expo SDK 58 upgrades React Native straight from 0.86 to 0.88, skipping 0.87. So: to try 0.87 in an Expo-managed project, switch to the expo@canary channel; to stay on the stable SDK, wait for the SDK 58 timeline that lands on 0.88.
Metro 0.87 and cache issues
With React Native 0.87, the Metro bundler was updated from 0.84 to 0.87. Because it's a big jump, the existing Metro cache is usually invalidated, and you may see strange module-resolution errors on your first build.
Following this order on your first post-upgrade build reduces the chance of trouble:
1watchman watch-del-all2rm -rf node_modules3rm -rf /tmp/metro-*4npm install5npx react-native start --reset-cacheIf "module not found" errors persist even after clearing the cache, try running "Clean Build Folder" in Xcode (./gradlew clean on Android) next; even when Metro's own cache is clean, the native build output can still carry references left over from the old version. Don't skip this on CI runners — a build that works locally can break in CI due to a different cache state there.
One more note: if you're following the 0.88 release candidates (as of this writing, September 23, 2026, 0.88.0-rc.2 is out), Metro's minimum version there bumps to 0.87.1. The release crew treats 0.88 as non-breaking in their own 0.88 notes — it doesn't change the strict API behavior mandated by 0.87, just adds small fixes on top (including reverting some TurboModule regressions). This doesn't affect a production move to 0.87 today, but if you track the RC in CI, set your Metro pin accordingly.
Experimental SwiftPM on iOS: should you try it
Along with 0.87, React Native introduced Swift Package Manager (SwiftPM) support on iOS at an experimental/preview stage — an alternative dependency-management path to CocoaPods.
Our recommendation is clear: don't fully migrate a production project to SwiftPM at this release. This isn't just our preference — the announcement's own list of known limitations says commands, flags, and the generated layout may change in future releases, and states in so many words, "Do not use it in production yet." CocoaPods remains the default and supported path; SwiftPM is opt-in and additive.
If you want to try it, the command injects Swift package references into your existing .xcodeproj file rather than replacing it; your signing, capability, and build-phase settings stay as they are.
1cd ios2# removes CocoaPods from the project3npx react-native spm --deintegrate4 5# to revert the change exactly6npx react-native spm deinitThe most common question: what if the library you use isn't published as a SwiftPM package? The announcement gives you a way out — if a community library doesn't ship a Package.swift, generate one from its podspec with npx react-native spm scaffold. Also run npx react-native spm once after a fresh clone and before building in CI; it's the equivalent of pod install.
Step by step with Upgrade Helper
React Native's official upgrade tool presents all native file differences between two versions (Xcode project, android/ folder, Podfile, etc.) as a diff. The logic: since React Native slightly changes its native skeleton every release, bumping the version number in package.json isn't enough — native project files also need diffing and syncing by hand.
React Native's official "Upgrading" docs emphasize that native project files need to be diffed independently of JS dependencies when jumping versions — files like AppDelegate, MainActivity, and build.gradle get small but critical changes each release, and those don't come automatically with npm install.
Practical flow:
- Note your current version and target version (0.87).
- Review the native file diff between these two versions in Upgrade Helper.
- Apply the
android/andios/changes to your project by hand — pay special attention tobuild.gradle,Podfile, andAppDelegate. - Update JS dependencies with
npm install, then runpod installfor iOS. - Run the TypeScript compiler and clear the deep-import errors (see the
grepscan above). - Verify toolchain versions (Node 22.13+, AGP 9, Kotlin 2.0+).
The order matters: updating JS dependencies before applying the native diff can leave your build tools in an incompatible version combination, throwing errors that are hard to make sense of.
Post-upgrade regression testing
The Strict TypeScript API change is caught at compile time, so once tsc --noEmit passes clean, you've seen most of the deep-import issues. But that's not enough on its own — you also need to verify runtime behavior, since the type-generation change may have also affected how some native modules are exported.
Recommended regression checklist:
- Type check:
npx tsc --noEmitshould produce zero errors in the project. - Deep import scan: rerun the
grepcommand above (without the quote restriction) and aim for zero matches. - Native module smoke test: test features that use native bridges — camera, push notifications, deep linking — on a real device.
- Metro cache clear: reset the cache on CI runners too and get at least one clean build.
- Android/iOS build: verify that the release build completes without errors on both platforms.
Splitting these into separate CI jobs helps you see faster which check broke: first a typecheck job (tsc --noEmit plus the deep-import grep scan), then parallel build-android and build-ios, then the smoke test on a device or simulator. Putting typecheck first catches type errors without paying the multi-minute cost of native builds.
A shortcut if you're coming from 0.85/0.86
If you're coming from 0.85 or 0.86, here's the good news: both releases already supported the Strict TypeScript API's opt-in preview, so if you'd already turned it on, you've likely done most of the deep-import migration work. What's left is the toolchain: bump Node to 22.13+, AGP to 9, Kotlin to 2.0+, and reset the Metro cache.
The practical benefit here is predictability: a team that already enabled the preview already knows which deep imports cause problems.
If you never enabled the preview, jumping 0.85→0.87 or 0.86→0.87 brings both the toolchain change and the type-system change at once. Splitting the migration into two separate PRs — first the toolchain upgrade, then the deep-import cleanup — makes review easier and lets you tell which change caused which regression.
There's one prerequisite you shouldn't skip: the React Native 0.82 announcement introduces that release as "the first React Native that runs entirely on the New Architecture," and says attempts to use newArchEnabled=false or RCT_NEW_ARCH_ENABLED=0 will be ignored. The same announcement names the last releases that still allow the Legacy Architecture: "React Native 0.81 or Expo SDK 54." So a project still on Legacy Architecture can't jump straight to 0.87 — it first needs to move to the New Architecture on 0.81 (or Expo SDK 54) and verify it works there; only then does the path to 0.87 open up. The removal of the useTurboModules feature flag in the table above reflects this same fact in 0.87. Curious about the New Architecture itself? We cover it separately in React Native New Architecture 2026: Fabric, TurboModules, and Bridgeless Mode; this article focuses only on the version/toolchain upgrade.
GOLDEN TIP
The most valuable insight in this article
This tip holds the article's most important takeaway.
Easter Egg
You found a hidden gem!
There's a hidden detail in this section. Want to uncover it?
Reader Reward
We gathered every command and checkpoint from this article into a single checklist. We put it together so you have it on hand on upgrade day — following it in order guarantees the migration is complete.
FAQ
What breaking changes are there in React Native 0.87?
The Strict TypeScript API became the default, so deep imports now produce a type error; the minimum toolchain moved to Node.js 22.13+/AGP 9/Kotlin 2.0+; Metro was updated from 0.84 to 0.87; and type definitions are now auto-generated from source. The announcement's "API removals" list also includes InteractionManager, NativeMethods/NativeMethodsMixin, *Properties type aliases, the Modal animated prop, and StatusBar backgroundColor/translucent; on iOS, namespace-less header imports (#import <RCTAppDelegate.h>) no longer compile.
Do I have to enable the legacy deep-imports opt-out?
No, it's not mandatory — and let's clear this up first: it's not an npm package, it's a custom condition added to the compilerOptions.customConditions array in tsconfig.json. There's nothing to install, and you don't run npm install for it. If your codebase is small and you can clean up all deep imports in one pass, you may not need this switch at all. In a large monorepo with dozens of deep imports, enabling the condition temporarily and migrating piece by piece as a team is the safer route.
What are the minimum Node and Gradle versions for RN 0.87?
You need Node.js 22.13+, Android Gradle Plugin (AGP) 9, and Kotlin 2.0+; the Kotlin version bundled with 0.87 is 2.2.0. For libraries, minCompileSdk is 34; on the app side, compileSdk/buildTools is 37. When moving to AGP 9, add the android.builtInKotlin=false and android.newDsl=false flags to android/gradle.properties.
Is Swift Package Manager support usable in React Native?
Yes, but at an experimental stage. SwiftPM support for iOS was introduced with 0.87 as opt-in and additive; CocoaPods remains the default and supported path. The announcement's known limitations say directly, "Do not use it in production yet." If you want to try it, start with npx react-native spm --deintegrate, and revert with npx react-native spm deinit.
Should I move to 0.88 or to 0.87?
As of this writing, September 23, 2026, 0.88 is at release-candidate stage (0.88.0-rc.2) and isn't stable yet. The release crew treats 0.88 as non-breaking in their own 0.88 notes, meaning it doesn't change the strict API requirement introduced by 0.87. Moving to 0.87 in production is a safe choice now; once 0.88 goes stable, follow up with a small patch upgrade.
If I'm using Expo, can I move RN directly to 0.87?
Not through a stable Expo SDK, no. The announcement is explicit: "For Expo projects, React Native 0.87 will be available as part of the expo@canary releases." So there's no stable SDK version carrying 0.87; to try RN 0.87 with Expo, switch to the expo@canary channel. On the stable side, per the Expensify/App#101427 issue, Expo SDK 58 moves React Native directly from 0.86 to 0.88, skipping 0.87 — so waiting it out is also a completely valid strategy.
Conclusion
The React Native 0.87 upgrade asks you to solve two independent tasks at once: updating the toolchain (Node 22.13+, AGP 9, Kotlin 2.0+, Metro 0.87), and cleaning up the deep imports broken by the Strict TypeScript API. Splitting these into separate commits makes the migration easier to track and easier to roll back.
None of the individual steps are complicated on their own — the difficulty is keeping the order right, taking each step only after confirming the previous one is green in CI: update the toolchain, clear the cache, scan for deep imports, pass the type check, then verify the native builds.
If you're curious about the architecture itself (Fabric, TurboModules, Bridgeless Mode), check out our React Native New Architecture 2026 article; this article focused only on the version and toolchain upgrade, not the architecture details. If you're running an Expo-managed project, we recommend cross-checking the toolchain section of our Expo SDK 52 EAS Build: Production Workflow article against the Node/AGP/Kotlin versions in this one — Expo's own SDK calendar can move independently of the RN core version.
Evaluating Flutter instead of React Native? Our React Native vs Flutter 2024 Comparison gives you the overall picture. Dealing with a separate wave of breaking changes on Android? See Android 17 (API 37): 6 Changes That Will Break Your App. We covered the same tightening trend on TypeScript's compiler side in TypeScript 7 Release.
Sources
- React Native 0.87 official announcement — the primary source for all of the release's breaking changes, August 11, 2026.
- Strict TypeScript API official documentation — technical explanation of the deep-import restriction and the root export list.
- React Native 0.80 announcement — the release where the Strict TypeScript API was first introduced as an opt-in preview, for cross-checking.
- React Native 0.82 announcement — the release where the New Architecture became the sole architecture; the source for the last releases allowing the Legacy Architecture (0.81 / Expo SDK 54).
- react-native npm package page (0.87.0) — release timing and package metadata.
- React Native GitHub Releases (v0.87.0) — official release notes and release-date confirmation.
- zyfolks.com — React Native 0.87 breaking changes analysis — independent secondary source, on Node 22.13+ and a CTO risk-mapping perspective.
- dopebase.com — RN 0.87 upgrade readiness check — practical pre-upgrade checklist, toolchain verification.
- Expensify/App GitHub Issue #101427 — an example of a real production team's decision on syncing Expo SDK and RN versions.
Tags
iOS Development News
Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.
We respect your privacy. You can unsubscribe at any time.

