Ship iOS and Android together.
Mobile apps built with React Native — shared code across platforms, native components where it counts, and the JavaScript ecosystem behind it.
React Native or Flutter, without the sales pitch.
Both are good, and both will build the app. The choice is usually decided by things that have nothing to do with rendering benchmarks.
- Your team. If you already have React developers, or a React web application whose types, validation and business rules can be shared, React Native compounds. Flutter introduces a second language into the company.
- The design. React Native maps onto real platform components, which suits an app that should feel like the platform it runs on. Flutter draws its own, which suits heavy custom design.
- The dependencies. Whichever ecosystem already has a maintained module for the SDK you have no choice but to integrate wins the argument outright.
- The web. React Native for Web can share code with an actual web application. That is a genuine advantage when the same product exists in a browser.
- Hiring, in your market and in ours. Both are common. Check which one you can realistically recruit for, because you will be maintaining this for years.
We build both, so we have no reason to sell one over the other. What we will not do is pretend the decision is obvious. When it is genuinely close we say so, and pick on the shape of your team rather than ours.
Expo or bare React Native.
This is the first real decision in a React Native project, and the default has moved. Expo is now the recommended way to start: it supplies the build service, the update service, a large set of maintained native modules, and a configuration system that generates the iOS and Android projects rather than leaving them as folders somebody edits by hand.
Expo, with development builds
Expo Go, the sandbox app, only runs the native code Expo itself ships. Real products outgrow it quickly, so we build a development build early: your own binary, with your own native dependencies, keeping the fast iteration loop. Native configuration is expressed as config plugins, which means an upgrade regenerates the native projects instead of forcing a manual merge. That one property removes most of the pain historically associated with upgrading React Native.
Bare
Native projects committed to the repository and managed by hand. It is the right answer when you are adding React Native to an application that already exists, when a dependency needs native changes no plugin expresses, or when a platform team insists on owning the build. The cost is that every upgrade is yours to merge.
Either way, this is decided in scoping with the dependency list in front of us — not discovered in month three.
Adding React Native to an app you already have.
You do not have to rewrite an application to use React Native in it. A React Native view can be embedded in an existing Swift or Kotlin app and used for one part of the product: a new feature area, an onboarding flow, or the section that changes weekly while the rest changes twice a year.
What that actually costs
- Two build systems and one release. The native pipeline has to produce and ship the JavaScript bundle as well.
- Navigation across the boundary: passing state in, getting a result back, and making the back button behave consistently on both sides.
- A shared design system, or the seam will be visible to users.
- Startup: initialising the runtime the first time a React Native screen opens, without the user watching an empty view.
- Team boundaries. Somebody has to own the bridge, or nobody will.
It is worth it when one part of the app moves much faster than the rest, or when a web team can now contribute to mobile. It is not worth it for a single screen, and we say so when that is the case.
Over-the-air updates, and where they stop.
A React Native app can download a new JavaScript bundle at runtime, so a fix reaches users without a store review. That is genuinely useful: a wrong string, a broken layout or a bad endpoint can be corrected the same day.
The limits, stated plainly
- Only JavaScript and assets update. Adding or upgrading a native dependency still needs a new store build.
- An update has to match the runtime version of the binary it lands on, so releases are tracked per runtime rather than as a single stream.
- Users get the update on their next launch, not instantly, and old versions stay in the wild longer than anyone expects.
- The stores allow this for fixes and improvements, not for changing what the app is. Shipping a different product over the air is a policy problem, not a clever move.
Used properly it is a safety net: staged rollout, an update that can be rolled back, and a real store release for anything structural. Used carelessly it becomes an untracked second deployment channel where nobody knows which code a given user is running. We keep that mapping explicit, in the release notes and in the crash reporter.
The New Architecture, and what it changes.
React Native has replaced its original asynchronous bridge. Fabric renders through the JavaScript interface directly, TurboModules load native modules on demand instead of all at startup, code generation makes the boundary between JavaScript and native typed, and Hermes is the default engine. Recent versions run this way out of the box.
For a product being built now this mostly means faster startup, smoother interaction under load, and fewer of the old timing bugs at the boundary. For an application that already exists it is a migration with real work in it — and the work is rarely in your own code. It is in the dependencies written for the old architecture.
- Every native dependency has to support the New Architecture, or be replaced.
- Some libraries run through a compatibility layer, which is a stopgap rather than a destination.
- Custom native modules you own are rewritten against the new interfaces.
- The upgrade is done as its own piece of work, with its own testing pass, rather than folded into a feature release.
Before a project starts we check the dependency list against it. It is an hour of work, and it is the check most likely to change the recommendation while changing it is still free.
Where the JavaScript ecosystem helps, and where it bites.
Where it helps
- One language across web, mobile and often the backend, so a small team covers more ground without splitting in two.
- Types and validation shared between a web app and the mobile app, so an API change breaks the build instead of a user's screen.
- Mature libraries for what every app needs: navigation, virtualised lists, forms, data fetching and caching, animation that runs off the JavaScript thread.
- Hiring is easier, because React developers are common and the mobile-specific part is learnable.
Where it bites
- A native dependency abandoned by its author becomes your problem at the next OS release.
- Transitive dependencies pull in native code you did not choose and now ship.
- Version matrices: React Native, the Expo SDK, the packages, Xcode and Gradle all hold opinions about each other.
- Patching a package to make it work is normal, and has to be recorded in the repository or it disappears at the next install.
So dependencies are treated as decisions. Each is chosen for how it is maintained as much as for what it does, the list is reviewed before it grows, and upgrades happen on a schedule instead of when something breaks. Performance follows the same discipline: virtualised lists, animation off the JavaScript thread, memoisation where it has been measured rather than sprinkled everywhere, and profiling with the React Native developer tools and the Hermes profiler on a real mid-range device.
Performance, and what done means.
Performance
- Virtualised lists for anything long, so the app renders what is on screen instead of everything it has ever fetched.
- Animation and gestures driven off the JavaScript thread, so a slow render never makes a swipe stutter.
- Images sized and cached properly — the most common cause of memory pressure on inexpensive Android devices.
- Fewer re-renders: state kept close to where it is used, and memoisation applied where a profiler showed it was needed rather than everywhere.
- Startup measured on a mid-range Android phone, because that is the device your store rating comes from.
Done
- Live on both stores, under your developer accounts, with your signing keys in your possession.
- Crash reporting and key events visible to your team, with over-the-air updates mapped to versions so a crash report says which bundle it came from.
- A repeatable release process, written down: store build, staged rollout, and how to roll back.
- The repository, the CI configuration and the third-party accounts in your name.
After that the app is maintained rather than finished: a maintenance release a few times a year for operating system and store requirements, dependency upgrades on a schedule, and a support path that does not depend on one person's memory.
The questions that come before a contract.
- React Native or Flutter?
- Mostly a question about your team, not our preference. If you already have React and TypeScript developers, React Native shares language, tooling and hiring pool with your web app, and the same people move between them. With no existing React investment, and if you want one rendering engine with an identical interface everywhere, Flutter is usually the calmer choice.
- Do you use Expo or bare React Native?
- Expo with TypeScript by default, on the New Architecture, with EAS for builds and submissions — it removes most of the native toolchain maintenance. When a native module or an existing native app requires otherwise, we move to a bare or prebuild setup, and the decision is written down with its consequences.
- We already have a native app. Can React Native go into it?
- Yes, brownfield integration works: new screens in React Native inside your existing iOS and Android app. Setup is heavier than a fresh project, because navigation, state and the build pipeline all have to straddle both sides, so we scope it as integration work rather than as feature work.
- Can you ship updates without waiting for store review?
- JavaScript-only changes can go out over the air, which is useful for fixes and copy. Anything touching native code still needs a store build and review, and over-the-air updates have to stay inside Apple and Google policy — they are not a way to change what the app does after it has been reviewed.
- How does working with a team in Tunis actually run?
- We are in Tunis on UTC+1, so the working day overlaps with Western Europe end to end, one hour apart at most during European summer time. We work inside your tools — your Slack or Teams, your issue tracker, your repository — with a demo of running software at the end of each sprint. You talk to the people writing the code, not to an account manager. Our working languages are English, French and Arabic.
- Who owns the code, and what if we want to continue alone?
- You own everything, and the repository is yours from the first commit. Handover means documentation, the build and release pipeline, store access and a walkthrough with your developers. We would rather leave behind a team that can maintain the app than one that cannot.