React Native in Production Across Four Engagements: Upgrade Log, Crash Rates, Store Outcomes, and the Native Modules We Wrote
What four production React Native apps run, what broke when we upgraded them, and the release discipline that keeps a bad build measured in hours.
2026-09-09 · By Filip Lauc
What Four Production React Native Apps Look Like From the Inside
Jaspero has React Native code running in four separate engagements: a high-volume consumer fintech app, a fitness community app, a delivery driver tool, and an activity discovery and booking client. Each one stresses a different part of the framework, which is why the failure modes we have seen barely overlap.
The fintech app is the volume case: a payments flow, a card-linked checkout path, and a support surface that cannot go down during business hours. The community app is push-heavy and social: sessions, bookings, notifications, and engagement loops. The driver tool is a logistics client that spends most of its life in the background with location tracking and a scanner open. The discovery client is map-first with heavy image work and a booking funnel.
That spread matters when you evaluate an agency, including this one. A team that has only shipped one category of app has only seen one category of failure. Background execution bugs on Android do not show up in a booking app. Payment sheet edge cases do not show up in a driver tool. Everything below is what those four codebases have actually cost us in upgrades, crashes, and store review cycles.
- • Consumer fintech: payments flow, checkout path, high daily session volume
- • Fitness community: bookings, notifications, social features, engagement loops
- • Delivery driver tool: background location, foreground services, barcode scanning
- • Activity discovery: maps, image-heavy feeds, booking funnel
The Upgrade Log: Hermes, Which Upgrades Broke Things, and Why We Deferred the New Architecture
Across the four apps we perform React Native version upgrades roughly twice a year per app, never skipping more than two minor versions at a time. The consistently expensive ones were the Hermes default switch, the Gradle and Android Gradle Plugin bumps that ride along with releases, and anything touching iOS build tooling after an Xcode major.
Hermes was the single biggest behavioural change. Moving off JSC surfaced code that had been quietly relying on JavaScriptCore specifics: Intl formatting differences in date and currency display, a date parsing path that accepted a non-standard string on JSC and returned Invalid Date under Hermes, and libraries shipping code that assumed a debugger protocol that no longer existed. The payoff was real, with faster time-to-interactive on cold start and lower memory pressure on mid-range Android hardware, which is what most of a delivery driver fleet actually runs. But it was never a one-line change. Every Hermes migration came with a full regression pass on formatting, dates, and any third-party SDK with a native side.
We evaluated the New Architecture (Fabric and TurboModules) on each app and deferred it on all four so far. The reasoning is not ideological. In each case the blocking factor was the same: a dependency we did not control, either a payments SDK, a maps or location library, or a media component, with no interop-safe release at the time we looked. Running the New Architecture with an interop layer wrapping half your native surface is a support burden with no user-visible payoff, and none of these apps were bottlenecked on bridge throughput after the optimisations already in place. We re-evaluate at each upgrade and write the decision down with the specific dependency that blocks it, so the client can see when the block clears rather than hearing 'not yet' indefinitely.
- • Cadence: roughly two React Native upgrades per app per year, never more than two minors skipped
- • Most expensive migration: Hermes as the default engine, mainly Intl, date parsing, and third-party native SDKs
- • Second most expensive: Android Gradle Plugin and Xcode major releases forcing native build config churn
- • New Architecture: evaluated per app, deferred on all four, blocked by specific uncontrolled native dependencies
- • Rule we follow: upgrade on our own schedule between feature milestones, never under release pressure
Crash-Free Session Rates and the Fixes That Actually Moved Them
We track crash-free sessions and crash-free users per release on every app and treat a regression in that number as release-blocking. The fixes that moved the needle were almost never in React code. They were native lifecycle issues, SDK initialisation order, image and list memory on low-end Android, and unhandled promise rejections surfacing as native crashes.
The recurring categories repeat across projects, so they are worth naming. First, Android background and process death: apps resumed from a killed process with a navigation state referencing data no longer in memory. The fix is defensive state rehydration, not a try/catch. Second, list and image memory: long feeds rendering full-resolution images on 3GB Android devices. Moving to a windowed list implementation with fixed item heights and correctly sized image requests removed an entire class of out-of-memory kills. Third, native SDK init races: a payments or analytics SDK initialised from JS after a deep link had already routed the user into a screen that assumed it was ready.
The operational discipline matters more than any individual fix. We ship with staged rollouts on Google Play and phased release on the App Store, watch the crash dashboard for the first cohort before widening, and keep the previous build ready to promote. Over-the-air updates are used only for JS-only regressions, and we are explicit with clients that anything touching native code cannot be hotfixed that way. That combination is why a bad release is measured in hours rather than a full store review cycle.
- • Android process death and defensive state rehydration after a background kill
- • Windowed lists with fixed item heights plus correctly sized image requests to stop OOM kills
- • Deterministic native SDK initialisation before any deep-link routing runs
- • Unhandled promise rejections converted into typed error boundaries with reporting
- • Staged and phased rollouts with crash-dashboard gates before widening to 100%
Store Review Outcomes: What a Rejection and Resubmission Actually Costs
One of the four apps was rejected on first App Store submission, fixed, and approved on resubmission. The cost of a rejection is rarely the fix itself. It is the review queue round-trip, which is why we now front-load a review-risk checklist before the first binary is ever uploaded.
The rejection categories we plan for are predictable: account deletion available in-app, sign-in options where third-party auth is offered, permission purpose strings that describe the user benefit rather than the technical reason, demo credentials that actually work for the reviewer, and any screen that could read as an unreviewed payment path. Google Play's equivalents are different: data safety declarations that match real SDK behaviour, background location justification with a recorded video, and target API level deadlines that arrive on a fixed calendar every year.
The practical scheduling advice we give founders is to treat first submission as a milestone with buffer in front of it, not something that happens on the day of a launch event. We submit the release candidate before marketing is locked, keep a reviewer test account seeded with realistic data, and answer review notes with a screen recording rather than prose. Resubmission after a clear single-issue fix has consistently been fast for us, usually days rather than weeks, but only when the fix and the explanation land in the same response.
- • In-app account deletion and matching sign-in options before first upload
- • Permission strings written as user benefit, not technical justification
- • A seeded reviewer account with realistic data, tested on the submitted build
- • Data safety declarations reconciled against every SDK actually linked
- • Review replies answered with a screen recording, not a paragraph
The Native Modules We Had to Write Because No Maintained Library Existed
React Native's ecosystem covers most needs, but four times across these engagements we wrote and maintained our own native module because the available options were unmaintained, incompatible with the version we were on, or heavier than the feature justified. Each one is a maintenance liability taken on deliberately, with a written exit plan.
The decision rule is simple. We write native code only when the alternative library has been unmaintained through at least one React Native major, or when the behaviour we need (background scheduling, a specific platform permission dance, a vendor SDK with no JS wrapper) cannot be expressed through an existing bridge. Anything else, we contribute a fix upstream or pin and patch instead. When we do write a module, it ships inside the client's repository under their ownership, with a scope note explaining what it does, which OS versions it targets, and what would let us delete it.
The categories we ended up owning were: a thin wrapper around a vendor SDK that shipped native-only, background location and delivery-state reporting on Android that needed foreground service handling the community libraries did not do correctly at the time, a device and integrity signal bridge used for fraud and support diagnostics, and small platform glue for share, deep-link, and notification-channel behaviour that differed between OS versions. None of them are large. All of them are documented, because a native module nobody understands is the thing that blocks the next upgrade.
- • Write native only when the library has been unmaintained through a React Native major, or the behaviour has no bridge at all
- • Prefer an upstream contribution or a pinned patch over a new module
- • Every module ships in the client repo, under their ownership, with a documented delete condition
- • Keep modules small enough that an upgrade does not require re-learning them
How to Evaluate a React Native Agency Using This Kind of Evidence
Ask any prospective React Native team for the same table this article publishes: which version each of their live apps runs today, how many upgrades they performed and what broke, current crash-free session rate per app, store review outcomes including rejections, and which native modules they wrote and why. A team that has run apps in production can answer without preparation.
The follow-up questions separate maintenance experience from launch experience. Who owns the Apple and Google accounts, and does the client hold them? What happens when Google raises the target API level requirement, and is that work inside the retainer or quoted separately? Can you hotfix a JS regression, and what is the over-the-air update policy given store rules? What is the rollback path for a native crash discovered at 20% rollout? Where does the release checklist live, and can we read it?
We publish this because 'we have React Native experience' is not a differentiator. Upgrade logs, crash dashboards, rejection histories, and rollout policies are all checkable artifacts. If you are shortlisting mobile teams, ask for evidence at that resolution from everyone on the list, including us.
Key Takeaways
- • Jaspero runs React Native across four production engagements: a consumer fintech app, a fitness community app, a delivery driver tool, and an activity discovery client.
- • The Hermes default switch was the most expensive upgrade in all four codebases, mostly Intl formatting, date parsing, and third-party native SDKs.
- • We evaluated the New Architecture (Fabric and TurboModules) on each app and deferred it on all four, each time blocked by a specific uncontrolled native dependency, with the block written down.
- • Crash-free session rate gains came from native lifecycle and memory fixes rather than React code: Android process death, windowed lists with sized images, and deterministic SDK init.
- • One app was rejected on first App Store submission and approved on resubmission, which is why a review-risk checklist now runs before the first binary is uploaded.
For the architectural layer underneath these operational numbers, see our write-up on architecting high-performance React Native apps, which covers state management, bridge traffic, and backend patterns at scale.
Frequently Asked Questions
Should I use Expo or bare React Native for a new app?
Start with Expo unless you already know you need a native module the managed workflow cannot express. Development builds and config plugins now cover most native integrations, and the prebuild escape hatch means the choice is no longer permanent. We go bare when a vendor SDK ships native-only with no plugin and the release cadence makes maintaining a custom plugin more expensive than owning the native projects directly.
How long does an App Store review take, and what happens if you get rejected?
Most App Store reviews complete within a day or two, and resubmission after a clear single-issue fix is usually similarly fast. The delay cost comes from rejections that require a product change rather than a config fix, or from a review note you answer with prose instead of a screen recording. Plan first submission with buffer in front of any launch date rather than on the day of it.
Is React Native still a good choice compared to Flutter or fully native apps?
React Native remains a strong choice when you want one product team shipping both platforms and your app is largely product UI, networking, and platform integrations, which describes most consumer and internal apps. Fully native still wins for heavy real-time graphics, deep OS integration, or apps where a specific platform SDK dictates the architecture. Flutter is a reasonable alternative; the deciding factor is usually your team's existing TypeScript and React skills.
Who should own the Apple Developer and Google Play accounts, the client or the agency?
The client should own both accounts, with the agency added as a member holding only the access it needs. This keeps signing certificates, store listings, and revenue tied to the business rather than the vendor, and it makes handover a permissions change instead of a migration. We set this up during onboarding and hand back full admin at the end of any engagement.
How much ongoing maintenance does a React Native app need after launch?
Budget for at least two React Native upgrades a year, plus Google Play's annual target API level deadline and whatever an Xcode major release forces. On top of that, expect periodic dependency and SDK bumps and an OS-release regression pass each autumn. Skipping this for eighteen months does not save money; it converts routine upgrades into a multi-week migration.
When is it worth writing your own React Native module?
Only when the maintained alternatives have gone stale through at least one React Native major release, or when the behaviour you need has no JS bridge at all, such as background scheduling or a native-only vendor SDK. Otherwise contribute upstream or pin and patch. If you do write one, keep it small, document the OS versions it targets, and record the condition under which you would delete it.
Sources
Written by
Filip Lauc
CEO, Jaspero
Filip Lauc is the CEO of Jaspero, a software development agency based in Osijek, Croatia. A full-stack JavaScript developer with over a decade of experience across Angular, Svelte, and Node.js, he leads Jaspero's work as a long-term embedded engineering partner for clients like GlycanAge, where his team has served as the dedicated engineering team for six years.
Let's Build Together
Your vision,
our expertise.
From AI integration to full-stack development, we turn ambitious ideas into products that perform.