Expo SDK 58 moves React Native from 0.86 to 0.88, React to 19.3, Reanimated to 4.7, Gesture Handler to 3 and jest-expo to Jest 30, and it is built for iOS 27. We upgraded ReadyNative, an Expo starter with 19 swappable module categories, in one pass, and ran every module combination through typecheck, lint, export and tests. This post lists what SDK 58 brings, then every error we hit, each with the exact message and the fix, so you can search for yours and skip ahead.
What Expo SDK 58 brings
The short list, each linked to its section of Expo's changelog:
- Built for iOS 27, including the UIScene life cycle that the iOS 27 SDK requires.
- React Native 0.88, which brings the 0.87 Strict TypeScript API as the default, fontVariationSettings on Text and ArrayBuffer in TurboModules.
- Home screen widgets on Android in expo-widgets, with their own JS bundle on a dedicated Hermes runtime.
- App Intents, to expose app actions to Siri and Apple Intelligence.
- Expo UI: recycling lists for large data sets, native navigation on iOS and new Compose components.
- Expo Router: stable data loaders and native tabs, and a navigation core with deterministic route keys.
- Expo Modules: prebuilt expo-modules-core for faster Android builds, and the Expo Modules 2.0 beta.
- A balanced fingerprint default, so fewer changes trigger a native rebuild.
- An experimental Rust transformer for about 2× faster cold bundling.
- Expo Observe and @expo/agent-cli, a CLI designed for coding agents.
Read the notable breaking changes before you start. The ones most apps feel: File.write() is async, NODE_ENV follows the command you run, R8 is on in Android release builds, Router projects default to server output, and Node.js needs 22.13 or newer.
How we upgraded
Start from Expo's pinned versions, not from the latest of each package. npx expo install expo@^58 --fix bumps everything Expo knows about, and npx expo install --check lists what is still off:
npx expo install expo@^58 --fix
npx expo install --check
# @types/jest@29.5.14 - expected version: 30.0.0
# @types/react@19.2.18 - expected version: ~19.3.0
# jest@29.7.0 - expected version: ~30.5.2Jest 30 is part of the upgrade: jest-expo 58 expects it. Third-party native libraries moved too. Sentry went from @sentry/react-native 7.11 to 8.28, the version SDK 58 pins, so read its changelog if you call its APIs directly.
Jest: "Could not locate module react-native/react-private-interface"
Configuration error: Could not locate module react-native/react-private-interface
mapped as: node_modules/react-native/$1Every component test failed before it ran. jest-expo 58 lists @react-native/jest-preset ^0.88.1 as a peer, but our lockfile still resolved 0.86.3, whose module map points react-native/react-private-interface at a folder that does not exist. The 0.88 preset maps it to src/react-private-interface.js. Bun kept the stale copy, so add the preset as a dev dependency at the React Native version:
"devDependencies": {
"@react-native/jest-preset": "0.88.1"
}Jest: "[Worklets] runOnUISync is not supported on web"
Next, every suite that imports Reanimated failed while it loaded react-native-reanimated/mock. With Reanimated 4.7 and Worklets 0.13 the worklets jest resolver alone sends Reanimated's initializers to the web build, which throws. Reanimated now ships its own resolver that wraps the worklets one:
// jest.config.js
module.exports = {
preset: "jest-expo",
resolver: "react-native-reanimated/jest/resolver", // was react-native-worklets/jest/resolver
};Jest: Sentry 8 ships ESM
With @sentry/react-native 8, tests that touch Sentry failed with a require-ESM error from jest-runtime, at jest.requireActual("@sentry/react"). @sentry/core and @sentry/react are ESM now, and jest only transforms what transformIgnorePatterns lets through. Widen the Sentry entry from @sentry/react-native to every Sentry package:
transformIgnorePatterns: [
"node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@sentry/.*|nativewind|react-native-css)",
],Jest 30: "Expected 0 arguments, but got 1"
Jest 30 checks toHaveBeenCalledWith against the mock's parameters. A mock typed as jest.Mock<() => Promise<T>> takes no arguments, so asserting that it was called with options is now a type error. Type the parameters:
getExpoPushTokenAsync: jest.Mock<
(options?: { projectId?: string }) => Promise<{ type: "expo"; data: string }>
>;
const mockScreen = jest.fn<(screen: string) => void>();Jest 30 also rewrites the snapshot header link, from goo.gl to jestjs.io. If you keep a copy of a snapshot anywhere, as our template does for the setup scripts, re-sync it.
RN 0.88: "Module react-native has no exported member FlexAlignType"
error TS2305: Module '"react-native"' has no exported member 'FlexAlignType'.React Native 0.87 made the Strict TypeScript API the default, and some helper types are gone. Derive the type from the style property instead of importing it:
import type { ViewStyle } from "react-native";
type FlexAlignType = NonNullable<ViewStyle["alignItems"]>;StyleSheet.NamedStyles is gone the same way. If you typed a style factory with it, declare a local version: type NamedStyles<T> = { [P in keyof T]: ViewStyle | TextStyle | ImageStyle }. To put the whole migration off for a cycle, Expo documents a tsconfig opt-out (customConditions with react-native-legacy-deep-imports), set to go away after React Native 0.89.
RN 0.88: "Cannot assign to padding because it is a read-only property"
Style types are read-only now. Code that builds a ViewStyle step by step fails on every assignment. Keep the object mutable while you build it:
// before: const base: ViewStyle = {};
const base: { -readonly [K in keyof ViewStyle]?: ViewStyle[K] } = {};
if (p !== undefined) base.padding = space[p];Also in this group, list props such as ListHeaderComponent no longer accept null. Pass undefined when there is nothing to render.
Tamagui on Expo SDK 58: style prop type error
With Tamagui 2.7, passing a React Native style prop to a Tamagui component fails on SDK 58. React Native 0.88's StyleProp includes void, and Tamagui's style prop accepts undefined, null, false and an empty string, but not void. Runtime behaviour is fine, so a typed cast in one place is enough until Tamagui's types catch up:
import type { StyleProp, TextStyle, ViewStyle } from "react-native";
// RN 0.88's StyleProp includes `void`; tamagui 2.7's style prop does not.
export function tamaguiStyle<Props extends { style?: unknown }>(
style: StyleProp<ViewStyle | TextStyle>
): Props["style"] {
return style as Props["style"];
}
<YStack {...props} style={tamaguiStyle<YStackProps>(style)} />NativeWind 4 and 5, Unistyles 3 and plain StyleSheet needed only the read-only and FlexAlignType fixes above. Our Tamagui with Expo guide covers the rest of the setup.
expo-auth-session: "Cannot find module expo-auth-session/build/QueryParams"
Supabase's Expo guides import getQueryParams from a build path. In SDK 58 expo-auth-session has an exports map that exposes only ./build/PKCE, and the root does not re-export getQueryParams. The function is small, so keep your own copy:
function getQueryParams(input: string) {
// The base only lets URL parse custom schemes such as myapp://auth-callback.
const url = new URL(input, "https://phony.example");
const errorCode = url.searchParams.get("errorCode");
url.searchParams.delete("errorCode");
const params: Record<string, string> = {};
url.searchParams.forEach((value, key) => (params[key] = value));
// The implicit flow carries its tokens in the fragment (#access_token=...).
new URLSearchParams(url.hash.replace(/^#/, "")).forEach((value, key) => (params[key] = value));
return { errorCode, params };
}Our Supabase auth with Expo post walks through the rest of that flow.
expo-notifications: "AndroidImportance not found in imported namespace"
error 'AndroidImportance' not found in imported namespace 'Notifications' import/namespaceThe enum still exists at runtime in expo-notifications 58.1, but ESLint's import/namespace rule no longer finds it on the namespace import. Import it by name:
import * as Notifications from "expo-notifications";
import { AndroidImportance } from "expo-notifications";
await Notifications.setNotificationChannelAsync("default", {
name: "Default",
importance: AndroidImportance.DEFAULT,
});Foreground notifications now show by default unless setNotificationHandler says otherwise, so check your handler too. The push notifications guide has the full setup.
expo-file-system: File.write() is async now
This one passes typecheck and tests and still breaks. File.write() now returns a promise. Our data export wrote a JSON file and opened the share sheet on the next line, which could share an empty file. Await it, or use File.writeSync() for the old behaviour:
const file = new File(Paths.cache, "my-data.json");
file.create({ overwrite: true });
await file.write(json); // SDK 57: file.write(json)
await Sharing.shareAsync(file.uri);iOS 27: drop your UIScene config plugin
On SDK 57 we shipped a config plugin that adopted the UIScene life cycle, because apps built with the iOS 27 SDK trap at launch without it. SDK 58's prebuild now generates SceneDelegate.swift and the UIApplicationSceneManifest entry itself. Our plugin still ran, and it replaced Expo's generated scene delegate with its own. If you wrote or copied a plugin like that, remove it from app.config.ts and delete it.
Patched dependencies: re-pin them
A patch applies to one exact version. We patch react-native-screens to reset the iOS header direction after an RTL session, and the fix is still not upstream in 4.30.1. The SDK pin is ~4.30.1, so pin the patched version exactly and rename the patch:
"react-native-screens": "4.30.1",
"patchedDependencies": {
"react-native-screens@4.30.1": "patches/react-native-screens@4.30.1.patch"
}What did not break
- Gesture Handler 3. The hook and callback renames only matter if you write gestures. Code that just wraps the app in GestureHandlerRootView is unchanged.
- Expo Router imports. Every expo-router import in the template resolved on 58. Read the router migration guide if you used the forked React Navigation APIs.
- Keyboard Controller 1.22, Safe Area Context 5.11 and expo-widgets 58. No code changes. On web, Expo Router now supplies zero safe-area insets, so use CSS env(safe-area-inset-*) there.
Upgrade checklist
- npx expo install expo@^58 --fix, then npx expo install --check.
- Add @react-native/jest-preset 0.88.1, and move to Jest 30 and @types/jest 30.
- Switch the jest resolver to react-native-reanimated/jest/resolver.
- Let @sentry/.* through transformIgnorePatterns.
- Fix strict-API type errors: FlexAlignType, read-only styles, null list props.
- Await File.write(), and check NODE_ENV and R8 in release builds.
- Remove any UIScene config plugin, and re-pin patched dependencies.
- Make a fresh development build: the native side changed.
Read next: jest-expo testing for the test setup this post patches, and the update-from-upstream docs for moving a project between SDK releases.
FAQ
What changed in Expo SDK 58?
SDK 58 is built for iOS 27 and moves to React Native 0.88 and React 19.3. It adds Android home screen widgets, App Intents, recycling lists in Expo UI, stable Expo Router data loaders and native tabs, and the Expo Modules 2.0 beta.
How do I fix "Could not locate module react-native/react-private-interface" in Jest?
Your project resolves an older @react-native/jest-preset. Add @react-native/jest-preset at your React Native version (0.88.1 on SDK 58) as a dev dependency and reinstall. jest-expo 58 lists it as a peer.
Why does Jest say "[Worklets] runOnUISync is not supported on web"?
The react-native-worklets jest resolver alone sends Reanimated 4.7's initializers to the web build. Set resolver to react-native-reanimated/jest/resolver in jest.config.js, which wraps the worklets resolver.
Does Tamagui work with Expo SDK 58?
It runs, but with Tamagui 2.7 passing a React Native style prop to a Tamagui component is a type error, because React Native 0.88's StyleProp includes void. A typed cast in one helper fixes it without changing runtime behaviour.
Can I stay on Expo SDK 57?
Yes. ReadyNative keeps an SDK 57 line on the expo-57 branch (v1.5.1-sdk57 at the time of writing). Move when your native dependencies support React Native 0.88, and take the SDK 58 release as its own step.