Tous les articles

Why We Build Our Mobile Apps with React Native and Expo

Par Grisú Studio ·

Why We Build Our Mobile Apps with React Native and Expo

Updated October 2026. The stack below reflects what every new app starts on today: Expo SDK 57, React Native 0.86 and React 19.2.

Grisú Studio makes utility apps and casual games for iPhone and Android: a QR code maker, a debt payoff planner, daily Stoic quotes, an emoji quiz, health calculators and about 60 others. Almost all of them are built with React Native and Expo. This post explains why, and what the setup looks like in practice.

The problem we are solving

A studio with one app can afford to treat it as a special case. A studio with dozens cannot. Each app has to be built, tested, given a store listing and a privacy policy, submitted to two stores, and kept working through every iOS and Android release after that.

So the question was never “which framework is best?” in the abstract. It was: what lets a small studio ship many small apps to both stores, keep them consistent, and still fix one quickly when something breaks?

Why React Native and Expo

One codebase, two stores. Each app is a single TypeScript project that ships to both the App Store and Google Play. Writing every app twice, once in Swift and once in Kotlin, would double the work for every feature and every fix, across the whole catalogue.

Expo handles the native plumbing. Expo’s modules cover most of what our apps need: SQLite storage, the camera and photo picker, audio, haptics and sharing. When something is not covered, such as a home-screen widget, a config plugin or a local Expo module fills the gap without forking the native projects.

The native projects are generated, not maintained. With expo prebuild, the ios/ and android/ folders are build output. Every native setting, from the bundle identifier to the tracking permission text, lives in app.json or in a config plugin. That one rule removes a whole class of “it worked on my machine” bugs, and it makes it possible to compare 60 apps’ native configuration by reading 60 JSON files.

TypeScript everywhere. Apps, tools, tests and this website are all TypeScript, so there is one language to keep sharp.

The stack every app starts on

Concern Choice
Framework Expo SDK 57, React Native 0.86, React 19.2
Language TypeScript in strict mode
Navigation expo-router (file-based routes)
State Zustand for global state
Records, history expo-sqlite
Settings, flags AsyncStorage
Animation React Native Reanimated
Ads Google AdMob via react-native-google-mobile-ads
Purchases expo-iap, for one-time purchases such as removing ads
Tests Vitest
Package manager pnpm

Some games add React Native Skia for shaders and custom drawing. Others are built in Godot instead, when a game needs a real game engine.

One skeleton, many apps

Every app is the same app underneath. A reference project holds the shared foundation: a small UI kit (buttons, cards, sheets, text, screens), the ads and consent layer, the share pipeline, translations, screenshot hooks and the tooling config. A script copies that foundation into each new app.

Two things are deliberately not shared:

  • The look. Each app owns its theme.ts and its fonts, and that file is the only place a colour, spacing value or font size may be written. A shared kit with one shared palette would make 60 apps look like one app, which neither users nor App Review appreciate.
  • The domain. Everything under src/features/<domain>/ belongs to that app alone.

The layout is the same everywhere, so moving between apps costs nothing:

src/
  app/                 expo-router routes, thin, no logic
  features/<domain>/
    components/ hooks/ logic/ types/
  lib/                 database, i18n, monetization, haptics…
  ui/                  the shared, app-agnostic kit

Pure logic, tested without a phone

The rule we care about most: src/features/<domain>/logic/ contains pure functions with no React, React Native or Expo imports. Scoring, timers, calculators, growth curves and payoff schedules all live there. ESLint enforces it.

That makes the logic testable in plain Node with Vitest, with no simulator involved. A typical app has somewhere between 70 and 140 tests, mostly on that logic. The UI calls the logic and never contains formulas.

One command decides whether a change is done:

pnpm verify   # typecheck, then lint, then tests

Nothing ships until it is green.

Native config in app.json, never in Xcode

Because ios/ and android/ are regenerated, a setting typed into Xcode or Android Studio survives only until the next prebuild, then disappears without a diff. We have been caught by this more than once. Now the rule is absolute: if it is native, it comes from app.json or from a config plugin in the app’s plugins/ folder.

A small plugin, for example, copies the app’s display name and App Store category into Xcode’s build settings on every prebuild, so what Xcode shows always matches what the binary carries.

Release builds: local, without EAS

We do not use EAS Build. Release builds are made locally:

  • iOS: expo prebuild, then an archive in Xcode, then a script that inspects the archive (SDK, version and build number against app.json, the AdMob app ID, permission strings, background modes) and refuses it before App Store Connect would.
  • Android: expo prebuild --platform android, then ./gradlew app:bundleRelease, which produces an .aab signed with that app’s own upload key.

Keeping the builds local means we always look at the actual artefact we are about to submit, and the whole release process is written down as scripts in the repository.

It has sharp edges, and they are worth knowing about:

  • expo prebuild deletes android/ entirely, including anything you put inside it such as the signing config. Restoring it is scripted and runs after every prebuild.
  • An incremental build can hide a broken dependency. One app kept producing signed bundles for days while its pinned ads SDK could no longer compile against React Native 0.86. Only a clean build revealed it. A green incremental build is not a reproducible build, and the lockfile, not the version range in package.json, is what actually ships.

Ads and privacy

Our apps are free and supported by AdMob ads, with a one-time purchase to remove them in some apps. The ads layer is shared across every app: the Google consent form where the law requires it, Apple’s App Tracking Transparency prompt asked at a sensible moment rather than on first launch, and a single gate that decides whether an ad may be shown at all.

Most apps keep everything on the device. There are no accounts and no servers of ours behind them, which keeps the privacy policies short and true.

What React Native is not the right tool for

For games where physics, particles and frame-perfect timing are the whole product, we use Godot. Some lighter games stay on the Expo kit with Reanimated and Skia, running their simulation on the UI thread, but React Native is a UI framework first, and we pick the engine per game.

Conclusion

React Native and Expo let a small studio do something that would otherwise take a much bigger one: ship many apps to both stores from one language and one shared skeleton, keep native configuration in version-controlled files, and test the logic that matters without a device. The parts that hurt, prebuild wiping the native folders and builds that are green only because they are incremental, are known and scripted.

If you are weighing the same choice for a catalogue of small apps rather than one big one, the shared skeleton and the pure-logic rule matter more than the framework itself. React Native and Expo just make both easy to keep.