Anthony Wentzel

Expo and Electron: one React Native codebase on the desktop

In 2024 we packaged an Expo web build inside Electron to ship React Native code as a desktop app. It works, with a few sharp edges.

From 2024 · 2 min read · Published September 12, 2026

Expo already gets you iOS, Android, and web from one codebase. In mid-2024 a project needed one more target: a desktop app that could be distributed outside a browser, including through the Mac App Store. We wanted to see how far the same code could go, so we tried Expo and Electron together: a React Native desktop app from an Expo web build.

The answer was: all the way, with Electron as a thin shell around Expo's web export.

The shape of an Expo Electron app

We used an Nx workspace with two apps:

  • apps/expo, the real app, which targets iOS, Android, and web as usual
  • apps/electron, a small shell whose only job is to load the web build in a native window

The Electron build is a script that exports the Expo web bundle and copies it next to the Electron entry point:

apps/electron/build.sh
set -e
 
# build expo for web
cd ../.. && yarn nx export expo
cd ./apps/electron
 
mkdir -p ./build
cp -r ./public/* ./build
cp -r ../expo/dist/* ./build
 
# absolute paths don't work from file://
sed -i '' "s#/_expo#_expo#g" build/index.html

The one line that matters

Expo's web export references its assets with absolute paths like /_expo/static/js/.... That's correct on a web server. Inside a packaged Electron app, the page loads from file://, where / means the root of the filesystem. Every script 404s and you get a blank window.

The sed line rewrites those paths to relative ones. It's a hack, but it's a well-understood one. A cleaner version would configure the base URL at export time, and that's the first thing I'd change.

Dev vs. packaged

In development, the Electron window loads the Expo dev server so you keep fast refresh. When packaged, it loads the local index.html. app.isPackaged tells you which mode you're in, and it's more reliable than checking NODE_ENV inside Electron.

Packaging itself is handled by electron-builder, with Nx targets for macOS (including a universal build for Apple Silicon and Intel), Windows, and Linux.

Sharp edges

  • Web is the ceiling. The desktop app can do what react-native-web can do. Any native module without a web implementation won't be there.
  • Security defaults matter. Keep contextIsolation on, expose only what you need through a preload script, and never disable certificate checks outside local development. It's easy to copy a permissive snippet while debugging and forget it's there.
  • App Store distribution is its own project. Mac App Store builds need sandbox entitlements and a provisioning profile, separate from direct-download builds. Budget time for it.

For us the big win was organizational, not technical. One team, one codebase, one design system, four targets.


Related: Upgrade Expo SDK safely: reproduce it in a sandbox first · Expo SDK 52 Android cold start: regression, or did the stopwatch move?