Installation
This guide walks you from a fresh Expo project to a working watchOS app rendering React via SwiftUI. If you already have an Expo iOS app, skip to Add the package.
Prerequisites
You'll need:
-
macOS with Xcode 16+ and the watchOS SDK installed (open Xcode → Settings → Components → install the watchOS simulator runtime).
-
Node.js 20+ and pnpm (or npm / yarn — examples below use
npx/pnpm). -
An Expo SDK 54+ project. If you don't have one yet:
npx create-expo-app my-appcd my-app -
Hermes enabled. Hermes is the default for new Expo apps; if you've explicitly disabled it, re-enable it in
app.json:{ "expo": { "jsEngine": "hermes" } }
Add the package
npx expo install @appsent-co/react-native-watchos @bacons/apple-targets
@bacons/apple-targets
provides the watch target itself (icons,
Info.plist, capabilities). This package owns the runtime (JS engine,
renderer, autolinking, bundle delivery).
Scaffold the watch target
npx react-native-watchos init
This runs Evan Bacon's create-target watch under the hood, then:
- drops a working
ContentView.swiftintotargets/<name>/— this is the SwiftUI entry that hosts the React root view, - scaffolds
index.watchos.tsxat the project root — the JS entry the watch will load, - patches
app.jsonto register the@appsent-co/react-native-watchosconfig plugin after@bacons/apple-targets. Plugin order matters: the watch target must exist before this package wires the Swift Package into it.
What just got generated
my-app/
├── app.json # plugin registered here
├── index.watchos.tsx # watch JS entry (← edit this)
├── targets/
│ └── watch/
│ ├── ContentView.swift # hosts the React root view
│ ├── Info.plist
│ └── expo-target.config.json
└── ...
Configure Metro
The watch bundle is served by the same Metro instance as your iOS
bundle, but at a different platform key (watchos). Wire the helper
into your metro.config.js:
const { getDefaultConfig } = require('expo/metro-config');
const {
withWatchosMetro,
} = require('@appsent-co/react-native-watchos/metro-config');
module.exports = withWatchosMetro(getDefaultConfig(__dirname));
This teaches Metro to:
- resolve
*.watchos.{ts,tsx,js,jsx}files for?platform=watchosrequests, - alias
react-nativeto a tiny watchOS-safe shim (the upstream module pulls in iOS-only code via lazy getters), - serve the watch bundle from the path the embedder expects.
Prebuild and run
npx expo prebuild -p ios --clean
npx expo run:ios
prebuild regenerates ios/ with the watch target attached. The
config plugin runs during this step — you'll see it install the
Release bundle build phase and run codegen for the
WatchConnectivity spec.
When the iPhone simulator launches, open the Watch companion window (Hardware → Devices → paired Apple Watch in older Xcode, or File → New Window → Apple Watch in Xcode 16+). Your watch app launches automatically alongside the phone app.
The first time you run, Hermes takes ~10 s to spin up on the watch. After that, Fast Refresh makes edits feel instant.
Verify it's working
You should see Hello from watchOS on the watch face. Edit
index.watchos.tsx:
import '@appsent-co/react-native-watchos/dev-support';
import { render, Text, VStack } from '@appsent-co/react-native-watchos/renderer';
function App() {
return (
<VStack>
<Text>Hello from watchOS 👋</Text>
</VStack>
);
}
render(<App />);
Save. The watch should update in place without a full reload.
Troubleshooting
The watch app shows a blank screen. Check the Metro terminal —
if there's no watchos bundle request, the embedder couldn't reach
your dev server. Confirm the watch and your Mac are on the same
network; the watch hits http://<mac-ip>:8081 by default.
Cannot find module 'react-native' on the watch. You forgot
withWatchosMetro in metro.config.js — the shim alias isn't
installed.
Fast Refresh stops working. The dev WebSocket dies on suspend.
Either shake the watch to reload (~2.3 g) or rerun expo run:ios.
Next steps
- Build a real screen — see Your first screen.
- Browse the Renderer overview for the full component surface.
- Send data from the phone — see Watch Connectivity.