Building Great React Native Apps with Expo in 2026
React Native with Expo is a sensible default for most mobile products in 2026. Here is how we structure, optimise and ship apps that feel native on iOS and Android.
React Native lets one team ship a single codebase to iOS and Android without the app feeling like a compromise, provided it is built with care. In 2026 the combination of React Native and Expo is, for most products we see, the sensible default: the tooling is mature, the native layer is accessible when you need it, and over-the-air updates make iteration fast.
This post describes how we approach React Native work at Amygdal: the architecture we start with, where performance actually goes wrong, how we handle unreliable networks, and the release process that keeps apps stable after launch. It is the longer version of an answer we give often in discovery calls.
Why Expo is the default now
A few years ago, teams treated Expo as a starter kit to be ejected from later. That is no longer how it works. With development builds, config plugins and EAS for building and submitting, you can keep the managed workflow and still add custom native modules. You get a consistent build pipeline, predictable upgrades and a much shorter path from a pull request to an installable build on a tester's phone.
Our Ealthiness app, a fitness companion with guided exercises, live run tracking and mood graphs, is built on React Native and Expo with TanStack Query on the client and a NestJS, PostgreSQL and Redis backend. Subscriptions run through RevenueCat and Stripe. None of that required leaving the Expo ecosystem.
Architecture: boring on purpose
The architecture that survives six months of feature requests is rarely clever. It is consistent. Our starting structure looks like this:
- Feature folders, not type folders. Each feature owns its screens, hooks, API calls and tests, so changes stay local.
- Server state in TanStack Query, local UI state in components or a small store. Most apps do not need a global state library for data that lives on the server.
- Typed API clients generated from the backend contract, so a renamed field fails the build instead of crashing in production.
- A small design system of primitives (text, buttons, inputs, spacing tokens) used everywhere, which keeps both platforms visually consistent.
- File-based routing with Expo Router, so deep links and navigation structure are explicit and easy to reason about.
The point is that a new engineer can open the repository and find where things live within an hour. That matters more to long-term velocity than any individual library choice.
Performance is a design decision
Janky lists and slow startup are what make cross-platform apps feel cheap. In our experience the causes are usually predictable, and they are almost never React Native itself:
- Lists that render everything. Use a virtualised list (FlashList or a well-configured FlatList), stable keys and memoised row components.
- Re-renders cascading from the top. Keep state close to where it is used and avoid passing new object or function references on every render.
- Animations on the JS thread. Run gestures and animations with Reanimated so they stay smooth even when the JS thread is busy.
- Heavy images. Serve correctly sized images, cache them, and avoid decoding large assets on screens that scroll.
- Bloated startup. Defer non-critical work, lazy-load rarely used screens and keep the initial bundle lean.
We profile on mid-range Android devices, not the newest iPhone on a developer's desk. If it is smooth there, it will be smooth for nearly everyone.
Offline and unreliable networks
Mobile users move between Wi-Fi, patchy 4G and no signal at all. An app that shows a spinner every time the network blips feels broken. Our default is cache-first reads with TanStack Query's persisted cache, so the last known data appears instantly and refreshes in the background.
For writes, the question is how much the product needs. Simple cases use optimistic updates with a retry. Apps where users create data offline, such as logging a workout mid-run, need a local queue that replays mutations when connectivity returns, plus a clear conflict strategy on the server. We decide this early, because retrofitting offline writes into an app designed for always-on networking is expensive.
Dropping to native where it counts
React Native shares most code across platforms, but some features deserve native treatment: background location for run tracking, camera pipelines, Bluetooth peripherals, health data integrations and complex gestures. With Expo modules, writing a focused native module in Swift or Kotlin is a contained task rather than an architectural shift. The craft is knowing which screens need it and keeping that native surface small and well-tested.
Releases that do not scare anyone
A reliable app is mostly a reliable release process. Ours includes:
- CI that type-checks, lints and runs tests on every pull request.
- EAS builds for internal testers on every merge to the main branch, distributed through TestFlight and the Play internal track.
- Over-the-air updates for JavaScript-only fixes, with a staged rollout and a rollback path.
- Crash and performance monitoring wired in before the first public release, not after the first bad review.
- A versioning policy so the backend can support older app versions that users have not updated yet.
That last point is often forgotten. Unlike a website, you cannot force every user onto the latest build, so API changes need to stay backward compatible or be gated behind a minimum-version check.
Auth, payments and the backend contract
A mobile app is only as good as the API behind it. We design the backend contract alongside the app rather than after it: endpoints shaped around screens where that saves round trips, pagination everywhere a list can grow, and explicit error codes the app can translate into helpful messages instead of generic failure toasts.
Authentication deserves the same care. Tokens live in secure storage, refresh happens silently in the background, and the app handles an expired session gracefully rather than dropping the user on a blank screen. For subscriptions, we use the platform stores through a provider such as RevenueCat so that receipts, renewals, refunds and cross-platform entitlements are handled correctly. Building that logic yourself is possible, but it is rarely where a product team should spend its time.
Testing what matters
We do not chase coverage numbers. Business rules get unit tests, important components get tests for their loading, empty and error states, and a short list of end-to-end flows such as sign-up, login and purchase run automatically before every release. That combination catches most regressions without slowing the team down.
Working with us on React Native
If you are planning a new app or inheriting one that has grown hard to change, we can help with architecture reviews, performance work or a full build. Read more about our mobile app development service, see how we think about mobile app engineering more broadly, or get in touch to talk through your product.
