React Native One Codebase: 60% Shared, Would Not Repeat

One React Native codebase served Android, iOS and web at about 60% shared code. Here is the split, the pinned versions, the release flow, and why I would not choose it again.
An EdTech math-enrichment platform used by thousands shipped Android, iOS and web from one React Native codebase. Every release went out as three builds: Expo built the iOS and Android candidates, and a GitLab CI pipeline I wrote compiled the web build. That was about two years ago. Roughly 60% of the code was shared. Payments were not. And after running it, I would not choose React Native for that job again.
TL;DR: On an EdTech math-enrichment platform used by thousands, one React Native codebase served Android, iOS and web. About 60% of the code was shared (my estimate) and payments had to be native. React was pinned 3 to 4 years behind current on purpose. Mobile releases went through EAS and TestFlight, web through a GitLab CI pipeline I wrote. The one-codebase design was inherited, and going native would have cost more time and budget, but managing three platforms from it got messy, and I would not choose React Native again.
Can one React Native codebase run iOS, Android and web?
Yes. A single React Native codebase shipped this platform to Android, iOS and the web, for six kinds of user: students, teachers, parents, school owners, admins and superadmins. It worked, and thousands of people used it. Getting it to run was not the hard part. Running it was.
The one-codebase design was inherited: the platform was built that way before it was handed over to my team, and going native would have needed more time and budget. So the choice stood.
The screens were math exercises loaded in grids, one set per grade. No math rendering, no drawing, no handwriting input. The backend was Flask and MongoDB, on infrastructure I owned as principal engineer. The same platform's move off AWS is its own story: what leaving AWS for a self-hosted cloud actually cost.
How much code does a React Native codebase really share across platforms?
About 60%, by my estimate rather than a line count. The other 40% was platform-specific work, and payments sat in it: they had to be native.
Both app stores publish rules for digital purchases (Apple's guideline 3.1.1 and Google Play's payments policy), and they are the first thing to read before planning a shared payments screen.
Splitting one component by platform is a documented pattern. Expo's docs put it this way: "Platform-specific extensions and Platform module can make the experience more native to a given platform." The page covers the .ios.tsx, .android.tsx, .native.tsx and .web.tsx extensions. The shape of it, illustrative, not the client's code:
components/
Payment.tsx shared fallback
Payment.ios.tsx iOS-only implementation
Payment.android.tsx Android-only implementation
Payment.web.tsx web-only implementationIn my view, each of these files is another place a change has to land.
Why pin React versions years behind the latest release?
Because newer React versions came with tons of compatibility issues. We kept React about 3 to 4 years behind current, on purpose. A known version running on all three targets was worth more than a newer one that might not.
Pinning React years behind current trades compatibility now for a bigger upgrade later. Every month the pin holds, that upgrade grows. When three targets share one codebase, one platform's compatibility problem holds back the other two.
How do you release one React Native app to the App Store, Google Play and the web?
With two pipelines. Mobile releases were planned and tested on EAS and TestFlight first, approved by hand, then rolled out automatically. Build notes were the manual part. Web went through GitLab CI, a pipeline I wrote, which compiled and deployed the web build.
Expo's docs define EAS Build as "a hosted service for building app binaries for your Expo and React Native projects." Apple describes TestFlight as the way testers "give feedback on your apps, games, and App Clips across Apple platforms before you publish." Between them they were the gate between a build candidate and a store. The command shape, illustrative, not the client's code:
eas build --platform all --profile production
eas submit --platform ios --latestExpo's submit docs are clear about the boundary: "EAS Submit uploads your binary but does not manage store listing metadata, screenshots, or release notes." They also note that "A TestFlight build is not automatically released to the App Store." Plan a human step for both.
Web always went through GitLab CI, a pipeline I wrote. For a different GitLab CI pipeline, see a Trivy and Snyk security gate in GitLab CI. Two release rhythms for one codebase are part of what it costs to run.
How does a React Native app handle live data: polling or WebSockets?
Both, each where it fit. The app talked to a Flask REST backend through TanStack Query (React Query) and custom endpoints. Most screens were plain REST, with polling wherever a screen needed fresh data. WebSockets carried the live class competitions and the test sessions.
React Query documents polling through refetchInterval: "If set to a number, the query will continuously refetch at this frequency in milliseconds." The shape, illustrative, not the client's code, with an arbitrary interval:
const { data } = useQuery({
queryKey: ['session', sessionId],
queryFn: () => fetchSession(sessionId),
refetchInterval: 5000,
});And a WebSocket subscription for a live session, also illustrative:
const socket = new WebSocket(sessionUrl);
socket.onmessage = (event) => handleUpdate(JSON.parse(event.data));The rule I'd keep: poll the screens that can be a few seconds stale, and use a socket where users share a live moment, such as a class competition or a test session.
Would you choose React Native for iOS, Android and web again?
No. React Native was a pain to work with, and managing multiple platforms from one codebase got really messy. Going native would have needed more time and budget, and that was the whole case for the shared codebase. In my view it did not outweigh the cost.
A mobile pipeline on EAS and TestFlight, a separate GitLab CI web pipeline, native payments, hand-written build notes and a React version held years back were each manageable alone. Together, they are why managing three platforms from one codebase got messy.
That is my verdict on this platform, which I inherited and ran. If someone hands me the same brief tomorrow, I won't start it with React Native.
Limitations
- The 60% is an estimate. It is my judgement, not a line count, and the other 40% is itemised here only as far as native payments.
- No exact versions. Exact React, React Native and Expo versions are not stated.
- Dates are approximate. The work was about two years ago. Current React Native and Expo tooling may behave differently.
- The web toolchain is not named. The web build was compiled in a GitLab pipeline I wrote.
- Not covered: login across mobile and web, which payment flows were native and why, and team size.
- No metrics. No load times, bundle sizes, bug counts or cost figures; "used by thousands" is the scale.
- One platform, no comparison. This is one platform, compared with no other build.
References
All read on 26 October 2026.
- Expo docs, Platform-specific modules.
- Expo docs, EAS Build and Submit to app stores (also at EAS Submit).
- Apple, TestFlight and the App Store Review Guidelines (3.1.1).
- Google Play Console Help, Payments policy.
- TanStack Query, useQuery (
refetchInterval).
More full-stack posts.
Sixty percent shared sounds like a saving until you count the pipelines. If you have shipped React Native to the web, what compiled your web build, and how did you keep React upgrades from breaking the other two targets?