Case study
Expo Watch Connectivity
A native Expo module that exposes Apple WatchConnectivity to React Native through a typed API.
Visit projectTech stack
- React Native
- Expo Modules API
- TypeScript
- Swift
- WatchConnectivity
- Kotlin
- npm
A native Expo module for Apple WatchConnectivity
I hit a gap while adding an Apple Watch app to Loadline. Apple's WatchConnectivity framework handles communication between iPhone and Watch, but Expo had no typed way to use it from React Native. I wanted the JavaScript side to feel ordinary to work with, even though activation, delegate callbacks and background delivery all lived in Swift.
So I wrote a native Expo module. Swift owns WCSession and its delegates. Expo Modules carries calls and events across the native boundary. A small TypeScript API lets React activate the session, inspect its state and subscribe to changes. I published the result on npm and GitHub so it could be used outside Loadline too.
Where React Native stops and Swift takes over
From React Native, the module looks like a normal package: import it, activate the session and subscribe to events. Underneath, Expo autolinking loads the Swift module and every operation crosses the native boundary. The companion Watch app has its own WCSession in native Swift. This package handles the iPhone side.
I kept the public TypeScript API small. It exposes current session state, activation, four transfer methods and focused listeners. The lower-level native module is still available when a developer needs it. The repository also includes a React hook, an example screen and the Swift code for the watchOS side.
Different data needs a different route
WatchConnectivity has four ways to move data, and they are not interchangeable. A live message fails when the Watch is unreachable. Application context keeps only the latest state. User info and files wait for background delivery. I kept those differences visible in the API instead of hiding them behind one generic send method.
| Use case | Module API | Delivery behavior |
|---|---|---|
| Live request and reply | sendMessage or sendMessageData | Requires a reachable counterpart and returns the reply to JavaScript. |
| Latest shared state | updateApplicationContext | Keeps the newest snapshot for background delivery. |
| Queued events | transferUserInfo | Queues every payload for delivery when the counterpart becomes available. |
| Larger payloads | transferFile | Runs as a background transfer with completion and progress information. |
The parts that needed native code
A paired Watch can still be unreachable
WCSession has more state than a connected flag can hold. The phone may support the framework without having a Watch paired. A paired Watch may not have the companion app, and an installed app can still be unreachable for a live message.
The module exposes support, pairing, installation, activation and reachability separately. When one of them changes, the Swift delegate sends a typed Expo event. React gets the new state without polling native code.
Pass a native reply handler through JavaScript
When the Watch sends a message that expects an answer, Apple gives the phone a Swift closure. JavaScript cannot hold that closure. The module stores it in native memory under a generated reply ID, then emits the ID with the message. JavaScript answers through a separate method that finds and consumes the original handler.
A lock protects the pending reply dictionary. If JavaScript never responds, the handler expires after 30 seconds instead of staying in memory.
Keep Android builds boring
WatchConnectivity only exists on Apple platforms, but Expo still autolinks the package in Android projects. Without an Android implementation, the package could break a build before the app ever called it. I added a small Kotlin module with the same surface. Support checks return false, and communication methods throw a clear platform error.
Expo SDK 54 later exposed compilation problems in that stub. Fixing them was a good reminder that an unsupported platform still needs to build cleanly.
Then I used it in Loadline
The package now sits under Loadline's phone-side Watch integration. A bootstrap component activates the native session once and listens for changes in pairing and reachability. The Watch app uses live messages for immediate workout actions and queued transfers for data that still needs to arrive after the connection drops.
Using the module in a real workout app forced it to handle the routine failures: a Watch goes out of range, the companion app is missing, the session changes state or a reply arrives through a native callback. The public API exposes those cases because callers need to respond to them.
Open source release
- Published on npm as @plevo/expo-watch-connectivity
- The GitHub repository had 26 stars and 3 forks in August 2026
- The examples cover React Native, a reusable hook and the native watchOS side
- The package is MIT licensed and includes typed APIs for state and data transfer
The API should not hide Apple's rules
The TypeScript API works because it keeps Apple's delivery rules visible. Each method makes a specific promise: live reply, latest state, queued payload or background file. Swift still owns the lifecycle and thread safety behind those promises.
The project also changed how I handle platform-specific packages. The native implementation is only part of the job. Autolinking, a buildable Android stub and useful examples determine whether someone can install the package without fighting it first.