Planning an Expo SDK 58 Upgrade: iOS 27, React Native 0.88 and New Tooling
A practical tour of the Expo SDK 58 beta: what changes for iOS 27 and React Native 0.88, which features are experimental, and how to test the upgrade safely.
This article is published in English.
Sources: Expo SDK 58 Beta: What React Native Developers Actually Need to Know
Expo SDK 58 is not defined by one headline feature. Instead, a long list of changes lands at once: iOS 27 support, React Native 0.88, faster Android builds, cheaper native module calls, Android home screen widgets, Siri integration through App Intents, a CLI aimed at coding agents, a more mature Expo Router, and an experimental Rust-based transformer for Metro. This guide sorts that list by practical impact, so you can tell which changes will touch your codebase, which ones you get for free, which are still experimental, and how to run the upgrade without breaking a production app.
Treat SDK 58 as a beta, because it is one
At the time of writing, SDK 58 is in beta, and Expo expects that phase to last roughly three to four weeks. Check the Expo changelog for the current status before you plan anything around a release date.
A beta period is the ideal window for testing: your application code, third-party libraries, custom native modules, build pipeline and any hand-written native code. It is a poor moment to bump the SDK on a production app just before a weekend. For side projects and internal tools, though, the beta is a low-risk way to see what is coming and to surface problems early, while there is still time for them to be fixed upstream.
iOS 27 and the scene-based lifecycle
A large part of the motivation for SDK 58 is iOS 27. Apps compiled against the iOS 27 SDK use Apple's scene-based lifecycle, and Expo has reworked its native infrastructure around that model.
Some background helps here. In the older model, UIApplicationDelegate owned the whole app: one window, one set of launch and foreground callbacks. The scene model splits those responsibilities, so each UI instance (a scene) has its own lifecycle, handled by a scene delegate. Work that used to live in the app delegate, such as reacting to the app becoming active or handling incoming URLs, can move to scene-level callbacks.
For a typical Expo project, this transition should be invisible. It matters when you have customized the native side. A common example is extra setup inside the app delegate's launch callback, like the Swift method below (shown here without syntax highlighting):
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Custom initialization
return true
}
If your AppDelegate.swift carries significant custom logic, some of it may now belong to scene handling rather than to a single global application window. Expo recommends reading its scene lifecycle migration guidance if you modify the native app delegate.
Which projects need a closer look
Plenty of Expo apps never open AppDelegate.swift, and for them the migration should be uneventful. Larger apps tend to accumulate native code over the years, though, and that is where the risk sits. Look closely if your iOS project has native setup for any of these:
- analytics SDK initialization
- deep link and universal link handling
- authentication SDKs
- push notifications
- native payment SDKs
Each of these typically hooks into launch or URL callbacks, which are exactly the callbacks affected by the scene model. Test those flows explicitly rather than assuming that a successful build means a working app.
React Native 0.88 and the Strict TypeScript API
The SDK 58 beta ships with the React Native 0.88 release candidate. Upgrading Expo therefore means absorbing everything that changed in both React Native 0.87 and 0.88, not just one release.
The change you are likely to hit first is the Strict TypeScript API, which tightens what counts as public React Native API. Imports that reach into internal files are the main casualty:
import Something from 'react-native/Libraries/...';
Apps and libraries have used deep imports like this for years, usually because the public entry point did not expose what they needed. It always worked by accident: internal file paths can move or change shape in any release without notice. React Native is now drawing a firm line between public and private modules, and a codebase full of deep imports should treat SDK 58 as the prompt to clean them up. Where a library in your dependency tree does this, check whether a newer version has already fixed it.
Removed APIs such as InteractionManager
Some older APIs are gone as well, and InteractionManager is one to search for. Code that defers heavy work until animations and interactions settle often looks like this:
InteractionManager.runAfterInteractions(() => {
loadHeavyContent();
});
The recommended replacement is the standard idle callback API:
requestIdleCallback(() => {
loadHeavyContent();
});
The semantics are close but not identical. InteractionManager waited for registered interactions to finish, while requestIdleCallback runs the callback when the JavaScript thread has idle time. For most "load this after the screen appears" cases the difference is irrelevant, but if you relied on it to wait for a specific animation, verify the behavior on a real device.
On a small app this migration is usually quick. On an older app with many dependencies, do the upgrade on a dedicated branch and let the TypeScript compiler list every breakage before you touch anything else. The compiler errors become your migration checklist.
Faster clean Android builds
expo-modules-core now ships precompiled native libraries for Android. Previously, a clean Android build spent a noticeable share of its time compiling the C++ code that Expo modules rely on. SDK 58 skips most of that step.
In Expo's own measurement, using an empty SDK 58 app on an Apple M3 Pro machine, a clean debug build for Android dropped from about 80 seconds to about 39 seconds. Your numbers will differ depending on project size and hardware, but the saving comes from skipping work entirely, so it should show up in any project using Expo modules.
The benefit multiplies in CI. A team that opens twenty pull requests a day, each triggering Android builds, pays that compile cost on every run, and CI machines often start from clean caches. Saving half a minute once is unremarkable. Saving it on every local clean build, every CI job, every feature branch and every release build adds up to real time and real compute cost. Build speed is a form of performance too, just one measured in developer minutes rather than frame rates.
Cheaper calls between JavaScript and native code
Performance work in SDK 58 is not limited to builds. Expo has also reduced the cost of calling into native modules, with most of the gains on iOS. Transferring long strings is significantly faster, and promises returned by async native functions carry less overhead.
You do not need to change anything to benefit: existing Expo modules pick up these improvements once you upgrade.
This matters most for apps that cross the JavaScript-to-native boundary frequently. Consider a call like this one:
const result = await SomeNativeModule.processLargePayload(data);
The native function may do its work quickly, but every invocation still pays to marshal arguments into native types and results back into JavaScript values. If an app makes such calls thousands of times, for example while processing a stream of sensor data or syncing large payloads, shaving a little off each crossing produces a measurable difference.
Expo Modules 2.0: native classes instead of a DSL
For teams that write their own native modules, Expo Modules 2.0 is one of the more significant changes. At the time of writing, it is in beta on both iOS and Android.
The current Expo Modules API describes a module through a DSL: a definition block where you declare functions, properties and events. Expo Modules 2.0 takes a more conventional approach. You write an ordinary Swift or Kotlin class and annotate the members that should be exposed to JavaScript.
The practical effect is that your native code reads like native code. Swift looks like Swift and Kotlin looks like Kotlin, without a framework-specific layer to learn first. That lowers the barrier for developers who know the platform but not Expo, and it helps AI coding tools as well, since they have far more ordinary Swift and Kotlin in their training data than Expo DSL code.
There is also a performance angle. Bindings can be generated at build time instead of being resolved dynamically at runtime, and Expo's benchmarks show large gains in several microbenchmarks. Microbenchmarks isolate a single operation, so they rarely translate one-to-one into app-level speedups; they are not a reason to redesign your native architecture on their own. If you want deeper background on why static bindings are faster, the comparison in Nitro Modules: how static bindings outperform React Native TurboModules covers the same principle. What stands out about Expo Modules 2.0 is that it makes modules both simpler to write and faster at once, which is a rare combination.
Home screen widgets on Android
expo-widgets now supports Android, which opens up product ideas for many kinds of apps: habit trackers, finance tools, delivery apps, fitness apps, task managers and calendars.
A habit app could show today's progress without being opened. A finance app might render a small budget summary, with a heading such as this one:
Monthly Budget
and a breakdown of the remaining amount by category underneath:
₹34,500 remainingFood ₹8,200
Travel ₹3,400
Shopping ₹5,100
A task manager could surface the three most important tasks for the day directly on the home screen.
On Android, each widget runs its own JavaScript bundle on a dedicated Hermes runtime, supports user interactions, and can use Material Colors so it blends with the system theme. The separate runtime is worth understanding: the widget does not share memory or state with your running app, so data has to be passed to it explicitly rather than read from your in-app store. For cross-platform teams, having widgets on both platforms inside the Expo ecosystem removes one more reason to maintain separate native implementations.
App Intents: Siri, Shortcuts and Spotlight
expo-app-intents connects Expo apps to Apple's App Intents system, which is the mechanism behind several system surfaces:
- Siri
- Shortcuts
- Spotlight
- Apple Intelligence
Picture a task manager. Normally, completing a task means opening the app, finding the item and tapping it. With an intent exposed, the user can simply ask Siri:
"Complete Upload ID documents."
Siri resolves the matching task and invokes the action your app has declared. Your JavaScript code receives that invocation and updates the app's data. In Expo's example, this happens without bringing the app to the foreground.
The broader implication is a shift in how a mobile app is structured. The app icon stops being the only entry point. Some actions start from Siri, some from a widget, some from Spotlight search and some from a user's Shortcuts automation. The app becomes a set of capabilities that the operating system can expose wherever they are useful, and that is worth considering when you design features.
One important caveat: expo-app-intents is in alpha at the time of writing. Experiment with it, but do not put a business-critical workflow on top of it yet.
Expo UI moves closer to SwiftUI and Jetpack Compose
Expo UI keeps deepening its integration with SwiftUI and Jetpack Compose. SDK 58 adds SwiftUI navigation primitives for iOS:
NavigationStack
NavigationLink
NavigationDestination
Toolbar
These map to SwiftUI's own navigation building blocks, so you get native stack navigation and toolbars rather than a JavaScript re-creation of them. The release also adds more Jetpack Compose components and refines the existing universal components that work across platforms.
A smaller improvement is surprisingly useful: every component in the Expo UI documentation now comes with a visual example. Without screenshots, choosing a UI component tends to follow a frustrating loop:
- Find a component that sounds right.
- Read through its props.
- Wire it into a screen.
- Launch the app.
- Discover it looks nothing like what you pictured.
- Start over with a different component.
Seeing the rendered component next to its API cuts that loop short.
Device Hub and browser-based simulator control
Xcode 27 replaces Simulator.app with Device Hub, and Expo CLI understands the new workflow. Expo is also experimenting with a separate package, expo-device-hub, which serves a local dashboard in the browser for your simulators and emulators.
From that dashboard you can view:
- a live mirror of the device screen
- CPU and memory consumption
- network traffic
- appearance options such as light and dark mode
- accessibility options
You can also tap, swipe and type on the device from the browser. To try it, install the package and start the dev server:
npx expo install expo-device-hub
npx expo start
If your day involves switching constantly between an editor, a terminal, the Android emulator, the iOS simulator and separate profiling tools, pulling some of that into one browser tab can make the development loop noticeably smoother.
Expo is also working on EAS Simulator, which aims to offer a similar experience with cloud-hosted simulators, so you would not need Xcode or Android Studio installed locally.
A CLI built for coding agents
This addition shows clearly where developer tooling is heading. Expo has released an experimental package:
@expo/agent-cli
Coding agents struggle when they have to decide which of several tools to call: Expo CLI, EAS CLI, expo-doctor and so on, each with its own flags and output format. The agent CLI wraps those behind one consistent interface. Setting it up for your project looks like this:
npx @expo/agent-cli@latest agents:setup
A few of its commands show the idea. The first one is:
status
It checks things like Expo Go compatibility without launching the app. The next one is:
dev
It starts the development environment and tries to pick the quickest route to a running app. The last one is:
smoke
It launches the app, takes a screenshot and shuts it down again.
That last command is the most interesting for automated workflows. Suppose you ask an agent to add a settings screen and confirm the app still launches. Instead of assuming its edit works, the agent can run the smoke check and look at the result. This is a small step from agents as code generators toward agents that can verify their own work, which is where most of their practical value lies. The package is experimental, so expect its commands to change.
Expo Router stabilizes several features
Expo Router gets a substantial update in SDK 58. Several features that were experimental are now stable:
- data loaders, for fetching route data before a screen renders
- server-side rendering for web output
- middleware that runs before a request reaches a route
- native tabs backed by platform tab bars
- toolbars
Under the hood, navigation has also been rewritten so that navigation state is more deterministic, meaning the same sequence of actions reliably produces the same state. Most apps should not notice a difference. If you built custom navigators or import directly from the following path, read the migration guide carefully:
expo-router/react-navigation
On the web, async routes are now the default. Each route can be split into its own JavaScript chunk and fetched when the user navigates to it, instead of shipping code for every screen on the first page load. For web apps built on Expo Router this is a sensible default, since initial load time is usually what users notice most. Keep in mind that it also means a route may briefly load on first visit, so loading states on the web deserve a quick review.
An experimental Rust transformer for Metro
Technically, this may be the most interesting change in the release, although it is explicitly experimental. Expo is testing a JavaScript transformer written in Rust called Noxcturnal, built on the oxc toolchain.
A large part of Metro's cold bundling time goes into Babel transforms: parsing each file, applying plugins and generating output. Replacing that step with native Rust code attacks the most expensive part directly. In Expo's tests, individual module transforms were about three times faster, and full cold bundles were about twice as fast. One large test app of roughly 5,500 files went from 5.26 seconds to 2.46 seconds.
You can opt in through an experiments flag in your configuration:
experiments: {
noxcturnalTransformWorker: true
}
Know the limits before you enable it. Projects that rely on custom Babel plugins, presets or specialized transforms may still fall back to Babel, so the speedup depends on how standard your setup is. Because the feature is experimental, its behavior and configuration may change between releases.
The direction is consistent with the wider ecosystem, where performance-critical JavaScript tooling has been moving from JavaScript to Rust for several years. Expo is now exploring the same path for Metro.
Fewer unnecessary native rebuilds
The default fingerprint behavior in SDK 58 is now the balanced preset. The fingerprint is a hash of everything that affects your native build; when it changes, Expo's tooling assumes a new native binary is needed.
That sounds dull until it spares you a native rebuild for an edit that never affected native code. Depending on the setup, changing something like the app version in app.json used to alter the fingerprint. The balanced preset tries to ignore changes that do not meaningfully alter the native runtime. The day-to-day result is simple: fewer rebuilds triggered for no good reason, which also means fewer unnecessary binary builds when you rely on fingerprints to decide between an over-the-air update and a new store build. No one demos this kind of feature, but everyone benefits from it.
Smaller improvements worth knowing about
Beyond the headline items, several module updates may be relevant depending on your app:
expo-cameraadds multi-page document scanning, useful for receipt scanners, document upload flows, identity verification (KYC) and expense management.expo-fontsupports variable fonts on Android, iOS and web.expo-audiogains playlist-aware lock-screen controls and WAV/PCM recording.expo-file-systemadds more hashing APIs and native preview support.expo-notificationsadds new behavior such as alarm-clock delivery on Android.- A PostHog integration can attach EAS build and update information to analytics events.
None of these defines the release on its own. Taken together, they show how wide the platform's coverage has become.
Breaking changes to review before upgrading
Upgrades are exciting right up until the app stops building, so do not skip this part. Before moving a real project to SDK 58, check the following.
React Native deep imports
Search the codebase for this path prefix:
react-native/Libraries/
Every match is an import of internal React Native files that the Strict TypeScript API may reject. Search node_modules too, or at least review failing dependencies, since libraries are a common source.
Removed React Native APIs
Confirm that neither your app nor its dependencies still use APIs that were removed across React Native 0.87 and 0.88, such as InteractionManager.
Custom AppDelegate code
If the iOS project contains native lifecycle customizations, review them against the scene-based lifecycle required by iOS 27.
Custom navigation in Expo Router
Custom navigators and imports from expo-router/react-navigation are affected by the reworked internals; follow the migration guide.
File system writes
File.write() is now asynchronous. Code that expects the file to exist on the next line will break unless it awaits the call. If you really need synchronous behavior, switch to:
File.writeSync()
Node.js version
The Node.js versions Expo supports have moved forward. Check your local environment and your CI images before assuming a failed build is an Expo bug.
A safe way to test the upgrade
Do not start on the main production branch. Create a dedicated migration branch and run the upgrade there:
npx expo install expo@next --fix
The expo@next tag installs the beta, and --fix aligns your other Expo packages with versions compatible with it.
Then test the unglamorous flows first, because those are the ones users notice when they break:
- Does the app launch at all?
- Does sign-in and authentication work?
- Can users move through the main screens?
- Do deep links open the right place?
- Do push notifications arrive?
- Can users upload images and files?
- Do payments complete?
- Does the Android release build succeed?
- Does the iOS release build succeed?
Only once those pass is it worth exploring the new features. New APIs are fun to try; regression testing is what keeps your users happy.
Where Expo is heading
Looking at SDK 58 as a whole, the direction matters more than any individual feature. Expo began as a way to make React Native easier to use, and it is steadily becoming a complete application platform:
- Expo Router for app structure and navigation
- Expo Modules for native APIs
- EAS Build for builds
- EAS Update for shipping updates
- EAS Observe for production monitoring
- Expo UI for native interface components
- widgets and App Intents for operating system integration
- agent tooling for AI-assisted development
Alongside those, Expo keeps investing in the infrastructure developers feel every day: faster builds, cheaper native calls, fewer rebuilds, better tunnels, better simulator tooling and possibly faster bundling. None of it makes for impressive screenshots, but it makes daily work better. For a broader view of how this positions Expo, see why Shopify's React Native exit doesn't threaten Expo.
Should you upgrade now?
For a production app, test before you commit. SDK 58 is still in beta, React Native 0.88 is included as a release candidate, several of the new APIs are experimental or alpha, and more fixes are expected before the stable release.
For a side project, an internal tool or a migration branch, exploring it now is well worth the time, especially if you need iOS 27 support or want to get ahead of the React Native changes. Problems you find during the beta can still be reported and fixed before the stable release, which is far easier than finding them after everyone else has upgraded.
The one command to remember is the same one you start with:
npx expo install expo@next --fix
Run it on a branch, not on production.
Key takeaways
- The changes most likely to break an upgrade are deep imports, removed APIs like
InteractionManager, customAppDelegatecode and custom Expo Router navigators. - Faster Android builds, cheaper native calls and fewer fingerprint-triggered rebuilds come for free with the upgrade.
- Expo Modules 2.0, the agent CLI, Device Hub and the Noxcturnal transformer are experimental or beta; evaluate them, but do not depend on them yet.
expo-app-intentsis alpha, while Android widgets make home screen features viable for cross-platform teams.- Upgrade on a dedicated branch, test the critical user flows first, and explore new features only after they pass.