React Native Development
One TypeScript codebase, two app stores, real native views on screen. We build React Native apps where the product roadmap is shared across iOS and Android — and we will tell you plainly when Swift or Kotlin is the cheaper answer.
What React Native is
React Native lets you write an app in React and TypeScript and render it with the platform's own view system — UIView on iOS, android.view on Android — rather than a web view. Since the New Architecture became the default, JavaScript runs on Hermes and talks to native code through JSI and TurboModules, so a native call is a direct function call instead of a message queued across an async bridge. What you do not get for free is the last mile: anything the platform exposes only to Swift or Kotlin still has to be written in Swift or Kotlin and wrapped.
Prefer the short definition? Read the React Native glossary entry.
The honest version
When React Native is the right choice — and when it is not
Choosing a technology because it is popular is how projects end up expensive. Here is where we would pick it, and where we would tell you not to.
Good fit
- One product roadmap, two stores, one small team
- When every feature has to ship on iOS and Android at the same time and the team is a handful of engineers, maintaining two native codebases means writing every screen, every form validation, and every analytics event twice — then reconciling the two when they drift. A shared codebase collapses that duplication for the bulk of an app — the lists, forms, navigation and network calls that make up most screens.
- Transactional and content-heavy apps: booking, ordering, chat, LMS, field data capture, internal ops
- These apps are made of platform-standard components — scroll views, text inputs, modals, tab bars. React Native renders exactly those. The hard parts are the API contract, the offline behaviour, and the state model, none of which get easier by writing them in Swift.
- Offline-first apps for patchy networks
- Apps used on the move — delivery, field service, sales, tier-2 and tier-3 learners on intermittent 4G — need a local database as the source of truth and a sync queue, not a spinner. SQLite through op-sqlite or WatermelonDB plus MMKV for hot key/value reads gives you that with the same code on both platforms, and the sync logic is written once.
- Products that already have a React or Next.js web app
- The domain types, Zod schemas, API client, and permission logic move across as a shared TypeScript package. Web and mobile stop disagreeing about what a valid order looks like, and a schema change breaks the build in both places rather than silently in one.
- Early-stage products still changing shape weekly
- Fast refresh keeps iteration tight, and JavaScript-only fixes can go out over the air through EAS Update rather than waiting on store review. Copy corrections, pricing logic, and bug fixes land the same day — which matters most in the months when the product is still being decided.
We would choose something else
- Heavy graphics, AR, video editing, and per-frame camera work
- A 3D scene, an ARKit/ARCore session, a frame-level video compositor, or on-device ML running over the camera preview all need to hold a 16.6ms frame budget in Metal, Vulkan, or a native pipeline. You can reach that from React Native — Skia and frame processors run on the UI thread — but by the time the render loop, the shaders, and the codec handling are all native, React Native is a shell around a native app. You have paid for a second build system and gained nothing. Write it in Swift and Kotlin.
- Apps whose value lives in OS extensions and background execution
- WidgetKit widgets, Live Activities, watchOS and Wear apps, share and keyboard extensions, CallKit/ConnectionService VoIP, and App Clips all run in separate processes that do not host your JS runtime. Long-running background work is the same story: iOS background modes are deliberately restrictive, and reliable Android behaviour means a foreground service or WorkManager written natively. If half the product is BLE that must survive app termination or geofences that fire when the app is dead, the React Native layer is the small part of the job.
- Low-latency audio, real-time drawing, and high-frequency sensor streams
- Audio DSP through AVAudioEngine or Oboe, an inking canvas that must track the pencil without perceptible lag, or a 200Hz sensor feed all break if data crosses the JavaScript thread. The mitigations exist — Reanimated worklets, JSI-native modules, C++ shared code — but they mean the performance-critical path is native anyway and you are debugging it through an extra abstraction.
- Vendor SDKs that ship native-only, especially in regulated finance
- App-shielding and RASP SDKs, payment terminal and mPOS SDKs, some DRM and device-attestation vendors publish only iOS and Android artefacts, sometimes with obfuscated binaries and their own build steps. You can write a TurboModule wrapper, but you then own that wrapper permanently: every vendor upgrade, every breaking signature change, every support ticket the vendor will not take because you are not on a supported integration path. On a regulated FinTech app dominated by such SDKs, native is the lower-risk build.
- An organisation that already has staffed iOS and Android teams
- React Native's economics come from one team shipping to two platforms. If there are existing Swift and Kotlin codebases with engineers who own them, adding React Native creates a third pipeline, an argument about who maintains the shared layer, and a brownfield integration where each native app embeds RN views. That is a legitimate architecture, but it is a migration project with organisational cost, not a shortcut — and it should be justified by something more concrete than wanting a shared codebase.
In practice
What we build with React Native
- Consumer and B2B apps for iOS and Android from a single Expo-managed TypeScript codebase, with EAS Build wired into CI and signed builds going to TestFlight and the Play internal track on merge.
- Offline-first mobile clients — SQLite as the local source of truth, a durable outbound mutation queue with idempotency keys, and conflict resolution decided on the server rather than guessed on the device.
- Native modules in Swift and Kotlin through the Expo Modules API or TurboModules, for the parts the JavaScript layer genuinely cannot reach: vendor SDKs, background services, platform capabilities not yet wrapped by the ecosystem.
- Mobile clients on top of an existing API, including the pass over the API surface that mobile always forces — pagination that survives a lost connection, payload sizes tuned for metered data, versioning so an old build on someone's phone keeps working.
- React Native views embedded into an existing native app (brownfield), where a specific flow is worth sharing across platforms and the rest of the app stays Swift or Kotlin.
Positions
Decisions we have already made
Defaults we start from. They are arguable — but they are argued, not assumed, and we will change them for a good reason.
- Expo with continuous native generation, not a hand-maintained ios/ and android/ folder
- We start every project on Expo and let prebuild generate the native projects from app config plus config plugins, rather than committing native folders. The reason is upgrade cost: hand-edited Xcode projects and Gradle files are where React Native version bumps go to die, and a two-year-old app stuck on an unsupported version is the most common mobile problem we get called about. Native changes are expressed as config plugins so they replay on every regeneration. When a vendor SDK genuinely cannot be expressed as a plugin, we commit the native folders deliberately and write down what we gave up.
- New Architecture on, and library choice gated on supporting it
- Fabric, TurboModules, and Hermes are the default, and a dependency that has not shipped New Architecture support is treated as a dependency we cannot take. This rules out some popular packages, and that is the point — an unmaintained native module is a load-bearing part of your app that no one can upgrade. We would rather write a small module ourselves in Swift and Kotlin than inherit an abandoned one.
- TanStack Query for server state, Zustand for the rest, MMKV for persistence
- Most of what people put in a global store is cached server data, and it does not belong there. TanStack Query owns fetching, caching, retry, and invalidation; Zustand holds the genuinely local remainder — session, draft input, UI mode — and stays small. Persistence goes to MMKV rather than AsyncStorage because reads are synchronous, which removes an entire class of first-frame flicker at app start. We reach for Redux Toolkit only when the app has real client-side state machines worth the ceremony.
- Animation and gestures on the UI thread; native stack navigation; FlashList for anything long
- Every gesture and animation goes through Reanimated worklets and Gesture Handler so it runs on the UI thread and keeps working while JavaScript is busy — a JS-driven animation stutters exactly when the app is doing work, which is exactly when the user is watching. Navigation uses the native stack, backed by UINavigationController and Fragments, so back gestures and screen transitions match platform behaviour instead of approximating it. Any list that can grow past a screenful uses FlashList, because FlatList virtualises without recycling — it mounts and unmounts cells rather than reusing them, so scroll cost grows with row complexity.
Architecture
Things worth getting right early
- Keep the JS/native boundary out of the hot path
- With JSI the boundary is cheap, but cheap is not free, and anything that must happen every frame — gesture tracking, camera frame processing, scroll-linked animation — should complete without a round trip to the JavaScript thread. We draw that line explicitly during design: which work is per-frame and stays in worklets or native code, and which work is per-interaction and can be plain React. Apps that feel janky in React Native are usually apps where nobody drew that line.
- The local database is the source of truth, not the cache
- For anything used on an unreliable connection, the UI reads from SQLite and writes to SQLite, and the network is a background reconciler. Mutations go into a durable queue carrying idempotency keys so a retry after a dropped connection cannot double-charge or duplicate a record, and the server decides conflicts. This is the design decision that determines whether the app is usable in a lift, on a train, or in a village with one bar of signal — and it has to be made before the first screen is built, not retrofitted.
- Two release channels, and only one of them skips review
- Native binaries go through EAS Build in CI and out via staged store rollout. JavaScript-only changes go through an over-the-air update channel, versioned against the native runtime so a JS bundle can never land on a binary that lacks the module it needs. OTA is for fixes and copy, never for changing what the app does — that is a store policy line, and crossing it gets apps pulled. Risky changes ship behind feature flags with a kill switch, so a bad rollout is a config change rather than an emergency build.
- Crash and performance telemetry, with source maps, from the first internal build
- A minified Hermes stack trace from a user's Redmi tells you nothing. We upload source maps and native symbols as a CI step, so crashes arrive readable with the release and device attached, and we watch startup time, JS frame rate, and slow screen transitions on real low-end Android hardware rather than on a simulator. Mobile performance problems concentrate on cheap devices, and those are frequently most of the install base in India.
Around it
What React Native usually sits next to
- React
The component model, hooks, and rendering semantics are the same — React Native swaps the host views, not the mental model.
- TypeScript
Types across the API boundary catch the mismatches that would otherwise surface as a crash on a user's phone, days after release.
- Next.js
When there is also a web app, domain types, validation schemas, and the API client live in one shared package instead of drifting apart.
- Node.js
A TypeScript backend lets the same engineers move between the API and the app, which matters when the team is small.
- Idempotency
Mobile networks drop mid-request constantly. Idempotency keys are what stop a retried sync from creating the same order twice.
- Feature flags
You cannot roll back a binary that is already installed. Flags give you a kill switch that works without a store submission.
Questions
Common questions about React Native
Is React Native slower than a native app?
For the kind of app most businesses build — lists, forms, navigation, network calls — the views on screen are the same native views, and users cannot tell. The differences show up in three places: app start time, which Hermes bytecode and a lean dependency tree largely address; long complex lists, which is why we use FlashList and virtualise properly; and anything running per frame, where a badly built React Native app will stutter and a correctly built one moves that work onto the UI thread. Where the frame budget is genuinely unforgiving — 3D, AR, video compositing, low-latency audio — native is faster and we say so.
Should we use Expo or plain React Native CLI?
Expo, in almost every case. The old objection was that Expo could not run custom native code, and that stopped being true once config plugins and the Expo Modules API arrived — you can add Swift and Kotlin and still keep generated native projects. What Expo buys you is a supported upgrade path, and upgrade path is the single biggest long-term cost in a React Native codebase. We only drop to bare workflow when a specific SDK forces it.
Can we ship updates without going through App Store review?
JavaScript and asset changes, yes — through an over-the-air update channel tied to the native runtime version. Anything that changes native code, adds a permission, or bumps a native dependency requires a new binary and a store submission. Both Apple and Google allow OTA updates for fixes and content; neither allows using them to change what the app fundamentally does, and treating OTA as a way to route around review is how apps get removed.
We only need an iOS app right now. Should we still use React Native?
Probably not, on that basis alone. The cross-platform saving is the whole argument, and on a single platform SwiftUI gives you a tighter toolchain and fewer moving parts. The exception is when Android is genuinely on the roadmap within a year or so, or when you already have a React web app and want to share the domain layer — then starting on React Native avoids a rewrite you have already decided to do.
Can React Native be added to an app we already have in Swift and Kotlin?
Yes — a brownfield integration embeds React Native views inside the existing native app, and it is a reasonable way to share one complicated flow across platforms without rewriting anything. It is worth being clear-eyed that it adds a JavaScript build and a second dependency tree to two codebases that currently do not have one, so it should be justified by a specific flow that is expensive to keep duplicated, not by a general preference for shared code.
Services that use React Native
Where it lands
Tell us what the app has to do
Send us the feature list, the platforms you need, and anything unusual — a vendor SDK, background location, a device the app has to talk to. We will tell you whether React Native is the right call or whether you are better off native, and why. If it is a focused piece of work, scope starts around Rs 25,000; larger products are quoted after we understand the architecture. Mon–Fri, 10:00–19:00 IST, from Ravet, Pune.