Using Expo OTA Updates to Ship UI Changes Fast Without App Store Rebuilds
Learn how Expo OTA updates let you push JavaScript and asset changes to users instantly, avoiding store rebuilds for UI tweaks and small feature updates.
24 Jul 2026, 22:48 UTC

The concrete problem
You have a React Native app built with Expo and need to tweak a piece of UI text—say, changing a button label from "Submit" to "Send". The usual path requires creating a new binary, running CI, waiting for App Store or Play Store review, and then asking users to download the update. Even a small change can take hours or days to reach users.
Thesis: OTA updates let you push JavaScript and asset changes instantly
Expo’s expo-updates library enables over‑the‑air (OTA) updates. After you build a binary once, you can publish new JavaScript bundles and assets to Expo’s servers. On the next app launch (or when you call Updates.checkForUpdateAsync()) the device downloads the new bundle and runs it, bypassing the store review loop.
How the workflow fits into a project
- Build a binary once – run
expo build:android -t apkorexpo build:iosto create a release binary that includes theexpo-updatesruntime. - Configure update behavior – edit
app.json(orapp.config.js) to set OTA options, for example:{ "expo": { "updates": { "fallbackToCacheTimeout": 0, "enabled": true } } }fallbackToCacheTimeoutcontrols how long the app waits for an update before using the cached version; setting it to0forces an immediate check on launch. - Publish an update – from your project directory run
This uploads the JavaScript bundle and assets to Expo’s update service associated with the binary’s release channel.expo publish --release-channel production - Device fetches the update – the next time the app starts (or when you invoke
Updates.checkForUpdateAsync()), theexpo-updatesmodule contacts Expo’s servers, downloads the new bundle if available, and loads it.
Worked example: changing a button label
Assume you have a component Button.js with:
import React from 'react';
import { Button } from 'react-native';
export default function SubmitButton() {
return <Button title="Submit" onPress={() => {}} />;
}
- Edit the file to change the label:
- From the project root, publish the change:
You will see output like "Published successfully!" and a URL to the update manifest.expo publish --release-channel production - On a test device running the previously built binary, close and reopen the app (or trigger a check via a debug menu that calls
Updates.checkForUpdateAsync()). The button now reads "Send" without any store interaction.
export default function SubmitButton() {
return <Button title="Send" onPress={() => {}} />;
}
You can verify which update is running by adding a temporary log:
import * as Updates from 'expo-updates';
Updates.getUpdateAsync().then(update => {
console.log('Running update:', update.updateId);
});
The console will show the identifier of the newly published update after the reload.
Trade‑off and limitation
OTA updates only affect the JavaScript bundle and static assets (images, fonts, etc.). Any change that touches native code—such as adding a new SDK, modifying AppDelegate or MainActivity, or linking a library that requires native linking—necessitates a new binary build and store submission. Additionally, if fallbackToCacheTimeout is set too high, a device may stay on an old cached version while a newer update is available, leading to inconsistent user experiences. You can inspect the effective timeout by reading the manifest at runtime via Updates.getManifestAsync().
Actionable closing
- Ensure your project uses
expo-updatesversion >= 0.14 (check withexpo install expo-updates). - Set
fallbackToCacheTimeoutto a low value (e.g.,0or1000ms) during development to see updates instantly. - Automate publishing: add a script
"publish:ota": "expo publish --release-channel staging"to yourpackage.jsonand hook it into your CI pipeline for every merge to a release branch. - After each publish, open the app on a device and confirm the change appears; if not, run
expo diagnosticsto verify the update service is configured correctly.
By treating the JavaScript layer as a deployable artifact, you gain the speed of web‑style iteration while still distributing a native‑feeling Expo app.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.