RORK LABJP
SDK58 — The Expo SDK 58 beta is open. It ships the React Native 0.88 release candidate, and the beta period is stated as three to four weeks11/01 — For anyone who requested an extension, Google Play's target API deadline lands on November 1. Forty-four days outEASENV — A long-open report: secrets handed to a local build arrive as the literal variable name rather than its value, and the damage surfaces much laterNEW — The replacement the table recommended had already shut down. A record of reconciling all 74 rows of the deprecation listUISCENE — iOS 27 requires the new scene lifecycle. SDK 57 makes it something you opt into; it only becomes the default in 58CREDIT — What "AI errors don't cost credits" actually covers becomes clear once you record a day of asking for the same fix more than onceSDK58 — The Expo SDK 58 beta is open. It ships the React Native 0.88 release candidate, and the beta period is stated as three to four weeks11/01 — For anyone who requested an extension, Google Play's target API deadline lands on November 1. Forty-four days outEASENV — A long-open report: secrets handed to a local build arrive as the literal variable name rather than its value, and the damage surfaces much laterNEW — The replacement the table recommended had already shut down. A record of reconciling all 74 rows of the deprecation listUISCENE — iOS 27 requires the new scene lifecycle. SDK 57 makes it something you opt into; it only becomes the default in 58CREDIT — What "AI errors don't cost credits" actually covers becomes clear once you record a day of asking for the same fix more than once
Articles/App Dev
App Dev/2026-08-03Advanced

When 1.10.0 Gets Locked Out: Measuring Four Version Comparison Approaches for Forced Updates

A remote-config gate for minimum supported version, rebuilt after measuring four version comparison approaches. Includes the cases where localeCompare reports equality, the fail-open boundary, and the incident caused by gating on a build still in review.

Expo209React Native238Remote Config10Versioning2App Operations6

Premium Article

A support message arrived a few days after I shipped version 1.10.0: the forced-update screen appears and there is no way past it.

My dashboard had the minimum supported version set to 1.9.0. Version 1.10.0 is obviously newer than that. There should have been nothing to block.

It did not reproduce on my own device. The iPhone running 1.10.0 sailed straight through. It reproduced the moment I pulled the gate's comparison function out and ran it under Node: "1.10.0" < "1.9.0" evaluates to true.

Compare them as strings and the second character decides it — "1" against "9" — so 1.10.0 loses. Obvious in hindsight. But for the eighteen months I spent inside the 1.9.x range, that condition never once came out false. The bug sat quietly and waited.

Running six apps as an indie developer, I keep meeting this category of defect: the one that only surfaces the instant a version segment reaches double digits. This time I rebuilt the comparison itself from measurements, so here is the record.

Four ways to compare version strings, three of which break

Start with the approaches that come to mind first: raw string comparison, localeCompare with the numeric option, parseFloat, and per-segment numeric comparison.

const sgn = (n) => (n < 0 ? -1 : n > 0 ? 1 : 0);
 
// (1) Plain string comparison
const naive = (a, b) => sgn(a < b ? -1 : a > b ? 1 : 0);
 
// (2) localeCompare with numeric collation
const loc = (a, b) => sgn(a.localeCompare(b, undefined, { numeric: true }));
 
// (3) Read it as a decimal number
const flt = (a, b) => sgn(parseFloat(a) - parseFloat(b));
 
// (4) Compare dot-separated segments as integers
function segs(a, b) {
  const A = a.split("."), B = b.split(".");
  const n = Math.max(A.length, B.length);
  for (let i = 0; i < n; i++) {
    const x = parseInt(A[i] ?? "0", 10) || 0;
    const y = parseInt(B[i] ?? "0", 10) || 0;
    if (x !== y) return sgn(x - y);
  }
  return 0;
}

I ran all four against version strings that actually show up in production. Results are from Node v22.22.3. A -1 means the left side is older, 1 means newer, 0 means equal.

Pair(1) String(2) localeCompare(3) parseFloat(4) Segments
1.9.0 vs 1.10.01-11-1
1.0 vs 1.0.0-1-100
1.02.0 vs 1.2.0-10-10
2.0.0 vs 2.0.0.1-1-10-1
1.4.0 vs 1.41100
1.4.0-beta.1 vs 1.4.01101
10.0.0 vs 9.99.99-1111
1.2.10 vs 1.2.9-1101
3.0.0 vs 3.01100
1.10 vs 1.9-11-11

Bold marks a result that contradicts what the gate needs. Approach (1) gets six of twelve pairs wrong; approach (3) gets five wrong.

My outage came from (1). Note that 10.0.0 vs 9.99.99 fails the same way — the moment a major version reaches double digits, every user of that app is classified as outdated at once.

parseFloat reads 1.2.10 as 1.2, so the patch segment disappears entirely. That is why 1.2.10 vs 1.2.9 comes back equal: everything after the second dot is treated as if it were not there.

Two places where localeCompare insists things are equal

Approach (2), localeCompare(b, undefined, { numeric: true }), handles every numeric case that broke (1) and (3). Both 1.9.0 vs 1.10.0 and 10.0.0 vs 9.99.99 come out right. Looking only at that, it seems sufficient.

I nearly rewrote the gate around it. Two things showed up once I put the results in a table.

First, it reports 1.02.0 and 1.2.0 as 0 — equal. Numeric collation reads runs of digits as numbers, so the leading zero vanishes. If you pad build numbers to a fixed width, two distinct versions become indistinguishable.

The second one does real damage. Comparing 1.4.0 against 1.4 returns 1 — the left side is treated as newer. A difference in segment count is read directly as a difference in magnitude.

If your Android versionName uses a two-segment form like 1.4, then the moment you write 1.4.0 as the remote threshold, devices on 1.4 are judged older than the threshold. Same version, different spelling, and they drop into the forced-update path.

This ran counter to my expectation. I had filed localeCompare under "compares numbers intelligently," when what it actually does is add a numeric reading rule to string collation. It knows nothing about version semantics. "Shorter is smaller when a segment is missing" is a lexicographic rule, not a versioning rule.

Approach (4) handles both of those correctly. It has its own defect: 1.4.0-beta.1 vs 1.4.0 returns 1. parseInt("0-beta") yields 0, the fourth segment 1 then wins, and a beta is ranked above the release. If you distribute prereleases at all, (4) is not usable as written either.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
A measured comparison table of four version-comparison approaches across 12 pairs, including where localeCompare calls 1.02.0 and 1.2.0 equal and ranks 1.4.0 above 1.4
Complete comparator code that passes all 16 test cases, measured at 286.4ms across 200,000 calls
A fail-open implementation for remote config, with the measured cutover boundary at a 1,500ms budget
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

Related Articles

App Dev2026-08-25
Put Your Live Rork App on a Spare iPhone Before iOS 27 Ships
iOS 27 arrives in September and developer beta 7 is already out. Here is how to turn an old iPhone into a beta device, what to check first, and which fixes you can ship from JavaScript without waiting for review.
App Dev2026-08-16
My chart broke on day one, not at scale
A line chart that vanished for anyone with only a few days of data. The cause was a zero-height Y axis turning coordinates into NaN. Here is the measured behavior and the small normalization layer that fixed it.
App Dev2026-08-06
Deciding overlay text legibility at ingest time instead of on device — four metrics measured side by side
Moving the question of whether text stays readable over a wallpaper out of the device and into the content pipeline. Four candidate metrics measured across 240 images, including what downscaled judging actually computes.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links