How to force update a React Native app
Last updated 20 September 2026
The moment a binary ships it is frozen, and some builds have to stop being used. This is what a correct version gate actually involves, with and without a service.
The problem, stated properly
You need three different answers, not one. Some builds must be blocked outright, because they corrupt data or hit an API you have removed. Some builds are merely old, and the user should be nudged. Everything else is fine and must not be interrupted.
A single "current version" check collapses those three into one and gets two of them wrong. What you want is a minimum supported version, a latest version, and a way to name specific builds that are worse than their version number suggests.
Comparing versions is where this goes wrong
Almost every home grown gate compares version strings in a way that fails eventually. The usual mistakes:
- String comparison. "2.10.0" sorts before "2.9.0" because "1" is less than "9". This is the single most common bug in a hand written gate.
- parseFloat. It turns "2.4.1" into 2.4 and silently drops the patch.
- Assuming three parts. Android build numbers are often four, and a marketing version may be just "2.4".
- Forgetting build numbers. iOS ships 2.3.1 (456) and 2.3.1 (457). If 456 is the broken one, a version comparison cannot tell them apart.
- Failing closed. If the version cannot be parsed, locking the user out is worse than letting them in. Fail open.
function compare(a: string, b: string): number {
const parts = (v: string) => v.replace(/^v/, '').split('+')[0].split('.').map(Number)
const left = parts(a)
const right = parts(b)
for (let i = 0; i < Math.max(left.length, right.length); i++) {
const l = left[i] ?? 0
const r = right[i] ?? 0
if (Number.isNaN(l) || Number.isNaN(r)) return 0 // unparseable: fail open
if (l !== r) return l > r ? 1 : -1
}
return 0
}Doing it yourself
Put a JSON file somewhere you can change without a release, fetch it at launch, compare, and render a modal. That is genuinely enough for a small app, and you should not adopt a service to avoid writing forty lines.
What it costs you later: the file has no history, so an incorrect minimum version is fixed by editing and hoping; there is no per platform split unless you build one; there is no way to block one bad build without bumping the floor and catching innocent versions with it; and the fetch has no caching story, so either it is slow or it is stale.
Doing it with Klor
The gate is configuration. You set a minimum supported version and a latest version per platform, and optionally a list of blocked builds. The SDK evaluates it on the device against the running version and hands you a verdict.
import { useVersionGate } from '@klor/react'
const gate = useVersionGate()
if (gate.status === 'forced') {
return <UpdateRequired message={gate.message} storeUrl={gate.storeUrl} />
}
if (gate.status === 'optional') {
return <UpdateAvailable {...gate} onDismiss={dismiss} />
}Klor ships no UI for this on purpose. It returns status, reason, the copy to show and the store link; the prompt is yours, so it looks like your app rather than like a vendor.
A blocked build is forced even when it is above the minimum supported version. That is the lever you reach for during an incident: pull 2.3.1 without moving the floor and without catching 2.3.0 and 2.3.2 in the same net.
Writing a prompt people do not resent
- Say what changed, not that an update is available. "Fixes a bug that lost drafts" earns a tap; "A new version is available" does not.
- Only force when you mean it. A forced gate has no way past it, which is the entire point and also the reason to use it sparingly.
- Make the optional one dismissible and remember the dismissal for a while. A nag on every launch trains people to tap through everything.
- Send them straight to the store listing, on the right store for the platform they are on.
- Never gate on a version you cannot parse. Locking out a user because your own version string was unusual is the worst possible outcome.
The incident playbook
The reason to set this up before you need it is that you will be using it at the worst possible moment. Written down, in order, so you are not deciding under pressure:
- Identify the exact build, not the version. "2.3.1 on iOS" is usually too broad; the bad artefact is often one build number of one version on one platform.
- Block that build rather than raising the minimum. Raising the floor to 2.3.2 also catches every 2.3.0 user, who were fine, and turns a contained problem into a support queue.
- Check the copy before you publish it. The forced message is the entire explanation your users get, and "Update required" tells them nothing about why.
- Publish, then verify from a real device on the bad build rather than from the dashboard preview.
- When the fix ships, unblock rather than leaving the entry in place. A stale block is a support ticket six months from now that nobody can explain.
Two things worth knowing before you rely on this. A blocked build is forced even when it is above the minimum supported version, which is exactly why it is the right lever. And nothing reaches a device until you publish, so staging the block and publishing it are two separate acts even in an incident.
What a gate cannot fix
A gate stops an old binary being used. It cannot make the replacement available. If the fix is still in App Store review, forcing an update leaves users with an app they cannot use and no way forward, which is worse than the bug for most bugs.
The order that works: ship the fix, wait until it is actually downloadable on both stores, then block the bad build. If you need to disable something before then, that is a feature flag, not a gate, and it is the reason to have both in one place.
Testing it before you need it
The reason gates fail is that they are written once and first exercised during an incident. Whatever you build, test all four paths before you rely on it: below the minimum, above the minimum but below the latest, current, and a version string that does not parse. Klor's dashboard has a field that runs the real evaluator against a version you type, including unsaved edits, so the preview cannot disagree with what ships.