Upgrading React Native
The Upgrade Helper diff, moving one Expo SDK version at a time, and why jumping four releases at once costs a week.
npx expo install expo@^57.0.0 --fix && npx expo-doctorReact Native ships a new minor version every few months, and Expo bundles a tested set of those into an SDK. Upgrading is not optional tidying. Stores require builds made with recent platform SDKs, and libraries drop old React Native versions within months of a release.
Expo projects
Move one SDK version at a time. Expo's own walkthrough says to upgrade incrementally, so that a breakage has one candidate cause rather than four. The sequence is: install the SDK, realign every other package to it, then ask the doctor what is still wrong.
npm install expo@^57.0.0
npx expo install --fix
npx expo-doctor
npx expo install expo@^57.0.0 --fix is shorthand for the first two lines. Same work, one command.
SDK 57 pins React Native 0.86, React 19.2.3 and Node 22.13.x. If the ios and android folders are generated rather than committed, delete them and let the next build recreate them. If you keep them in the repository, run npx pod-install and apply the native changes by hand.
Bare projects
The Upgrade Helper at react-native-community.github.io/upgrade-helper renders the diff between any two React Native versions: Gradle files, Podfile, Gemfile, iOS and Android entry points. You apply it file by file. React Native 0.87 also raises floors of its own, including Node 22.13.0, Kotlin 2.0 and Android compileSdk 37.
What goes wrong
Skipping four versions merges four sets of breaking changes into one failing build. A development build is native code too, so the client compiled for the old SDK cannot run the upgraded project.
Example
// Not a component: a plain helper that prints the upgrade plan for a project.
// Expo's walkthrough recommends upgrading one SDK version at a time, so the
// plan for SDK 54 -> 57 is three separate upgrades, not one.
type SdkStep = {
from: number;
to: number;
command: string;
};
export function upgradePath(current: number, target: number): SdkStep[] {
if (target <= current) {
return [];
}
const steps: SdkStep[] = [];
for (let version = current; version < target; version += 1) {
const next = version + 1;
steps.push({
from: version,
to: next,
// Shorthand for: npm install expo@^<next>.0.0 && npx expo install --fix
command: `npx expo install expo@^${next}.0.0 --fix`,
});
}
return steps;
}
const plan = upgradePath(54, 57);
console.log(plan.map((step) => `SDK ${step.from} -> ${step.to}: ${step.command}`).join("\n"));
// Output:
// SDK 54 -> 55: npx expo install expo@^55.0.0 --fix
// SDK 55 -> 56: npx expo install expo@^56.0.0 --fix
// SDK 56 -> 57: npx expo install expo@^57.0.0 --fixWhen to use it
- Moving a shipping app from Expo SDK 54 to 57 without losing a week to a single unexplained build failure.
- Deciding whether a React Native minor release needs a native rebuild or only a JavaScript bundle update.
- Preparing a bare project for a new Xcode or Gradle requirement before the store deadline arrives.
More examples
One SDK upgrade, start to finish
The whole Expo upgrade, in the order the docs give it. Reach for this on a branch of its own, with nothing else in the diff, so that a red build points at the SDK and not at your week's feature work. If the ios and android folders are committed instead of generated, replace step 4 with npx pod-install plus the native edits the release notes list.
# Step 1: the SDK itself. One version per branch, per pull request.
npm install expo@^57.0.0
# Step 2: realign every other package to the versions SDK 57 was tested with.
npx expo install --fix
# Steps 1 and 2 in one command, identical result:
# npx expo install expo@^57.0.0 --fix
# Step 3: what is still wrong. Checks package.json, app config, dependencies.
npx expo-doctor
# Step 4: native projects. With Continuous Native Generation they are outputs,
# so throw them away and regenerate.
rm -rf ios android
npx expo prebuild --clean
# Step 5: a new development build. The old one was compiled against SDK 56.
eas build --profile development --platform allA bare project, applying the Upgrade Helper diff
What upgrading looks like without Expo's prebuild step. The Upgrade Helper is a diff viewer, not a tool that edits your project: you read it and copy the changes across, skipping lines that belong to template code you have already replaced. Build both platforms immediately, because a missed Gradle or Podfile line fails at compile time rather than at runtime.
# No Continuous Native Generation here: ios/ and android/ are source files you
# own, so nobody can regenerate them for you. The Upgrade Helper prints the
# diff between two React Native versions, file by file:
# https://react-native-community.github.io/upgrade-helper/?from=0.86.0&to=0.87.0
git checkout -b upgrade/rn-0.87
# React Native 0.87 raises the toolchain floor before it raises anything else.
node --version # must be >= 22.13.0
npm install [email protected]
# Then apply the diff by hand. The files it almost always touches:
# android/build.gradle compileSdk 37, Kotlin 2.0+
# android/app/build.gradle
# android/gradle/wrapper/gradle-wrapper.properties
# ios/Podfile
# Gemfile
npx pod-install
# Build both platforms before you touch any product code.
npm run ios
npm run androidThe three versions that must agree
An excerpt of package.json after upgrading to Expo SDK 57. These three versions are not independent choices: SDK 57 pins React Native 0.86 and React 19.2.3, and the SDK's own reference table is where those numbers come from. Bumping react-native by hand to a version the SDK does not pin is the most common way to break an Expo project, because every expo-* package is built against the pinned one. The engines field documents the Node floor for anyone cloning the repo, and npx expo-doctor checks the installed versions against the table for you.
{
"dependencies": {
"expo": "^57.0.0",
"react": "19.2.3",
"react-native": "0.86.0"
},
"engines": {
"node": ">=22.13.0"
}
}
Discussion