Oct 13, 2026
5 min read
- Open Source
- React Native
- Expo
- Kotlin
- iOS
- Android
React Native and Expo let one team build iPhone and Android apps with the same code. What makes them innovative, the fixes I contributed to Expo and React Native, and what I learned.

The projects
In one sentence: React Native lets developers build iPhone and Android apps with the same code, and Expo is the toolkit that makes building, testing and publishing those apps much easier.
Traditionally, a company that wants an app on both iPhone and Android pays for two apps: one written in Swift for Apple, one in Kotlin for Google. React Native, created at Meta, changed that: you write the app once in JavaScript and React, and it shows real native screens on both platforms — not a website disguised as an app. Apps you may use every day are built this way.
Expo builds on top of it. It gives ready-made building blocks for things phones do — camera, microphone, notifications, images, location — and it can build and publish the apps in the cloud, so you don't even need a Mac to ship an iPhone app.
What makes it innovative
- One app, two platforms. A single team builds for iPhone and Android at once, and users still get a real native app.
- Phone features made simple. Camera, microphone or notifications become a few lines of code instead of weeks of platform-specific work.
- No special computer needed. Apps can be built and sent to the App Store and Google Play from the cloud.
- Bugs checked by a robot. Expo uses an automated assistant that tries to reproduce each reported bug and checks proposed fixes — I saw it at work on my own contributions.
In short: my contribution and what I learned
My part: I proposed 18 fixes; 4 are already part of Expo and the others are being reviewed (none of my React Native fixes is accepted yet). The accepted ones fixed real problems for app makers: an audio recording that could freeze forever, a camera on the web that said "ready" before it actually was, a build that crashed on Windows computers, and an app crash on phones with an empty SIM card name.
What I took away: on mobile, the hardest bugs come from timing and from the differences between devices and browsers — something that works on one can fail on another. And being clear about what you could and couldn't test earns reviewers' trust.
The rest of this article goes into the technical details, for developers.
Under the hood
React Native (126k stars, MIT) — Fabric renderer, JSI, TurboModules; Expo (52k stars, MIT) — Expo Modules API in Swift/Kotlin, CLI, Expo Router, config plugins and EAS.
In detail: what I brought
My contribution in numbers: 18 pull requests — 4 merged in Expo, 9 more open there, and 4 open in React Native. To be clear: none of my React Native PRs is merged yet.
Merged in Expo
- expo-audio (Android, Kotlin). With background recording on,
prepareToRecordAsync()could hang forever: a binding timeout existed but was never started. Starting it raised a second problem — the timeout runs on one thread, the service callback on another, and a coroutine continuation can only be resumed once. The maintainer pointed out a remaining check-then-act race; the final version takes and clears the continuation atomically, then rejects and unbinds. - expo-camera (web).
onCameraReadyfired before the<video>had a frame, sotakePicturethrew "camera not ready". It now waits for the stream to be attached and for real data. A first version waited forHAVE_ENOUGH_DATA, which WebKit never reaches in this case; after the bot's run I switched to "a current frame and a width". Result: 25/25 passes on Chromium, Firefox and WebKit, versus 25/25 failures before. - expo-widgets (Windows). Metro's resolver stubbed every non-file name as an empty module — including the absolute Windows path of the empty module itself, which looped until a stack overflow. Recognizing
D:\…paths as files fixed it, confirmed by the bot on a Windows VM. - expo-cellular (Android).
simOperator?.substring(0, 3)crashed when the operator was an empty string:?.guardsnull, not"".
In review
- expo-image: a 7200×7200 JPEG crashing with "trying to draw too large bitmap", and SVG decoders that claimed every file — reading whole videos into memory.
- Android core: a launch intent dropped when the app restarts, which made
getLastNotificationResponseAsync()returnnull. - iOS task-manager: races between the launch thread and a finishing task, including a possible use-after-free.
- React Native: a one-word iOS fix —
unsignedIntValuetruncatedUIDataDetectorTypeAll(a 64-bit "all bits" value) to 32 bits, sodataDetectorTypes="all"did nothing on the New Architecture;Symbol.toStringTagonBlob,FileandURLso libraries like axios recognize them; and TypeScript types for theperformanceglobal.
Technical lessons
- Native bugs are usually concurrency or size bugs. Two threads resuming one continuation, a 64-bit flag stored in 32 bits, a bitmap bigger than the GPU allows. The JavaScript surface rarely shows where the real problem is.
- Test across engines, not just browsers you like. My first camera fix worked in Chromium and failed half the time in WebKit. Measuring readiness the way every engine reports it was the actual fix.
- Optional chaining is not validation.
?.protects againstnull; empty strings, partial values and wrong lengths still need explicit checks. - Be honest about what you could run. I couldn't run some Android fixes on a device and said so in each PR. Automated verification and CI filled part of the gap; the rest is for reviewers to decide.
- Large projects need patience. Four merged PRs in a framework this widely used taught me more about review culture than any tutorial: small diffs, a changeset, and a clear "before / after".