Convert a React App to a Mobile App (2026): The Four Things That Break First
“Convert my React app to a mobile app” is really two different requests wearing the same words, and mixing them up wastes months.
The first request is a rewrite: take the product and rebuild its UI in React Native, so the phone renders real native components. You keep your business logic, but the JSX and styling are new and you now maintain a second codebase. Where we stand on that path: our cloud source builds compile Flutter, native Android (Gradle), and Capacitor projects today, while React Native, Ionic, and Expo source builds are waitlist-only — details in our React Native cloud build post.
The second request — the one this guide covers — is a wrap: your React SPA is already deployed at a URL, and you want that exact app installable from a phone's app drawer, with an icon, a splash screen, and push notifications. That takes minutes, not months. The deal is explicit: you get a signed Android APK/AAB whose native shell renders your live site in a WebView. It is your deployed app in a native skin — not a native rewrite — so it inherits both the good (instant updates on every deploy) and the bad (no offline mode, WebView performance equals mobile-Chrome performance).
Mechanically, the wrap is the boring part: paste the URL, set the app name, package ID, and a 512×512 PNG icon, optionally connect OneSignal for push, and the pipeline compiles and signs an APK/AAB in the cloud in a few minutes — the docs cover the pipeline, and our general website-to-app guide covers those steps. The rest of this post is the React-specific part: an SPA inside a WebView fails in four predictable places, and every one of them is checkable before you spend a build credit.
The pre-flight checklist: where React SPAs break inside a WebView
1. Client-side routing: BrowserRouter is fine — if your server plays along
The WebView loads your deployed URL over HTTPS, exactly like mobile Chrome does. That means the old “you must use HashRouter in a WebView” advice mostly does not apply here — that rule is for wrappers that bundle files locally and serve them from file://. With a hosted URL, BrowserRouter works — under one condition: your host has to return index.html for every route it doesn't recognize.
Here is why this bites harder in an app than on the web. In a browser, users mostly arrive at your root URL and navigate client-side, so the server rarely sees /settings/billing as a real request. An app changes the traffic pattern: Android can kill the app in the background and cold-restore it on the route the user left off, and a push notification can deep-link straight into an inner route. Both produce a direct server hit on a path that only exists client-side. Without an SPA fallback, your app's first frame is a 404 page. On nginx the fix is one line:
location / {
try_files $uri $uri/ /index.html;
}On Netlify it's a _redirects file containing /* /index.html 200; on Vercel, a rewrite in vercel.json. The ten-second test: open a deep route in a browser and refresh. A 404 means fix the host — or, on a static host where you genuinely cannot configure rewrites (classic GitHub Pages), switch to HashRouter, which keeps the route after a # so the server only ever sees the root document. Ugly URLs, zero server config.
2. The Android hardware back button
This is the detail that separates a wrap that feels like an app from one that feels like a kiosk. Android users navigate with the system back gesture constantly, and the naive behavior — back instantly exits the app — reads as broken.
The shell's contract: on back, it checks whether the WebView has history to step through and calls goBack() before ever closing the app. React Router cooperates out of the box, because every client-side navigation goes through the History API — each pushState creates a real history entry, so the back button retraces your in-app routes exactly as a user expects. You don't need to write any bridge code for this.
What you do need to audit is your own history discipline, because a WebView makes sloppy patterns visible:
- Redirects must replace, not push. The classic auth guard —
navigate("/login")when unauthenticated — pushes the guarded page onto history. User logs in, presses back, lands on the guard, gets bounced to login again: a back-button loop. Usenavigate("/login", { replace: true })so the dead-end page never enters history. - Decide what back means for modals. If a modal or drawer opens without a history entry, back closes the whole page under it — jarring in an app. Push an entry when the modal opens (or use your router's modal-route pattern) so back dismisses it first.
3. Base path and asset paths: the white-screen classic
If your React app is served from a subpath — example.com/app/ rather than a root or subdomain — the most common failure is a build that assumes root: the HTML loads, then every hashed JS and CSS asset 404s because the bundle requests /assets/index-Ab3xQ.js instead of /app/assets/index-Ab3xQ.js. In a browser that's a blank tab with a console full of red; in a WebView it's a silent white screen, because your users can't open devtools. In Vite, set base: "/app/" in vite.config.ts; in legacy Create React App it's the homepage field in package.json. The simplest de-risking move is structural: serve the app from its own subdomain (app.example.com) and the whole class of problem disappears.
4. Service workers: don't let January's bundle haunt July's users
A WebView honors service workers just like Chrome does, and that cuts both ways. The trap has a specific shape, familiar from the Create React App era of serviceWorker.register(): the service worker precaches index.html, which references hash-named chunks. You deploy; the old cached index.html keeps requesting chunks that no longer exist, and users are stuck — sometimes on a flat-out broken app. On the web a user eventually hard-refreshes. In a wrapped app there is no refresh button, so the failure is stickier and the support ticket says “the app is broken,” not “the cache is stale.”
The audit takes a minute: if you don't deliberately need offline behavior, make sure you're calling the unregister path, not register. If you do run a service worker, serve index.html and service-worker.js with Cache-Control: no-cache and use a network-first strategy for the document, so a deploy propagates on next launch. And to be clear, a wrapper adds no offline support of its own — if offline is a hard requirement, see the comparison next and our web-to-app vs PWA breakdown.
The real alternative: Capacitor
For a React codebase specifically, the credible competitor to a hosted-URL wrapper is not another wrapper service — it's Capacitor, the free, MIT-licensed native runtime from the Ionic team. Instead of pointing a shell at your deployed URL, Capacitor copies your production build output into a generated native project, so the JS bundle ships inside the binary and you get a plugin ecosystem for camera, filesystem, background tasks, and offline storage. It is the better engineering answer for a genuinely app-shaped product. It is also a real commitment: as of mid-2026 you own an Android Studio project and an Xcode project, every release is a rebuild plus a store review, and shipping iOS still requires a Mac with Xcode — Capacitor itself costs nothing, but the toolchain and release process are yours to run.
| Question | Hosted-URL wrapper (this post) | Capacitor |
|---|---|---|
| Where does the React code run? | Your server — the app loads the live URL | Inside the binary — bundle copied at build time |
| Shipping an update | Deploy your site; installed apps get it on next launch | Rebuild, resign, resubmit through store review |
| Local toolchain needed | None — cloud build from a URL | Android Studio; Xcode on a Mac for iOS |
| Offline / native plugins | Online-only; push via OneSignal | Full plugin ecosystem |
| Cost of the tool | First build free; plans $29–$199/mo | Free and open source; your time is the cost |
Our take: if you need native capability, pick Capacitor and accept the toolchain — our Capacitor vs native comparison goes deeper on that fork. If what you need is presence — an icon, a Play Store listing, push re-engagement — wrapping the URL gets you there this afternoon without adopting a native project. The two even compose: wrap the URL to ship this quarter, move to Capacitor when offline requirements arrive. And if you already have a Capacitor project with an android folder and just don't want to run Android Studio, our source-build pipeline compiles and signs those in the cloud too.
And iOS? Read this before you budget for it
We are deliberately precise here, because this is where web-to-app services tend to inflate. For Android, the deliverable is a signed, store-ready APK/AAB — done. For iOS, every cloud .ipa build (signed or unsigned) sits on the Pro plan or higher, and there are three honest routes:
- Default: unsigned .ipa. Built on Apple silicon, handed to you unsigned. You re-sign with your own Apple Developer certificate ($99/year to Apple) and submit yourself.
- Cloud signing with your certificate. Upload your .p12 to an encrypted vault and get a cloud-signed .ipa back. Still your certificate, still your submission — we never ask for your Apple ID or App Store Connect access, and we don't submit to the App Store for you (a managed submission service is planned, not live).
- Web Clip: the zero-signing route. A tap-to-install home-screen icon for iPhone users, no certificate or review at all.
One more checkpoint: Apple's guideline 4.2 rejects thin repackaged websites. A React app with real accounts and functionality has a fair shot; a wrapped landing page does not.
FAQ
Is this the same as React Native?
No. React Native compiles JavaScript into an app that drives real native UI components — a separate codebase from your web app, sharing logic but not markup. This guide covers wrapping your deployed React web app in a native WebView shell, which reuses your existing site as-is. Our cloud source builds currently compile Flutter, native Android (Gradle), and Capacitor projects; React Native, Ionic, and Expo source builds are waitlist-only as of mid-2026.
Do I need to switch to HashRouter for the app to work?
Usually not. The app loads your deployed site over HTTPS, so BrowserRouter behaves exactly as it does in mobile Chrome — it works as long as your host rewrites unknown paths to index.html. Configure that SPA fallback (Netlify _redirects, vercel.json rewrites, or nginx try_files) and keep your clean URLs. HashRouter is the escape hatch for static hosts where you cannot configure rewrites at all.
What happens when users press the Android hardware back button?
The shell asks the WebView to step back through its browsing history before closing the app. React Router creates a real history entry for each client-side navigation via pushState, so the back button walks your in-app routes the way users expect. The gotchas are in your code, not the shell: perform redirects with a replace navigation instead of push so users don't get trapped in a back loop, and give modals or drawers a history entry if back should close them rather than leave the page.
Do I need to rebuild the app every time I deploy my React app?
No. The shell renders whatever is live at your URL, so every deploy reaches installed devices on the next launch with no store review. You rebuild only when the native shell itself changes: app name, icon, package ID, or push configuration. One caveat: if your build registers a service worker, a badly configured one can pin users to a stale bundle — serve index.html and the service worker file with no-cache headers so new deploys actually land.
Can I get an iOS app from my React site too?
Yes, with honest caveats. All cloud iOS .ipa builds require the Pro plan or higher. The default output is an unsigned .ipa that you re-sign with your own Apple Developer certificate; on Pro and above you can instead upload your own .p12 signing certificate to an encrypted vault and receive back a cloud-signed .ipa built on Apple silicon. We never ask for your Apple ID or App Store Connect access, and we do not submit to the App Store for you. The iOS Web Clip is the zero-signing route if you just need a home-screen install on iPhones.
What does it cost to wrap a React app?
The first Android build is free — one credit, with a watermark on the splash screen. Paid plans: Starter $29/month for 10 build credits ($290/year), Pro $99/month for 30 credits plus cloud iOS builds ($990/year), Agency $199/month for 100 credits with white-label branding and Windows EXE builds ($1,690/year). Failed builds auto-refund their credit, a $49 add-on prioritizes an urgent build, and we accept Stripe or USDT — details on the pricing page. Google Play's developer account is a separate $25 one-time fee; Apple's developer program is $99/year if you take the App Store route.
Run the checklist, then spend zero dollars finding out
Refresh a deep route, press back through your flows, check your base path, audit the service worker — then paste your URL and put the free first build on a real Android phone. Twenty minutes end to end, and you'll know how your React app feels installed before paying anything.
Code2Native Engineering
Engineering team
Written by the Code2Native engineering team — the people who build and operate the cloud build pipeline.