Add native code only after defining the smallest JavaScript-facing contract. The boundary should expose a product capability, not mirror an entire platform framework.
Choose the Smallest Native Surface
- Use an existing maintained Expo or community module first.
- Use an Expo Module for native methods, events, constants, and platform services.
- Use a native view when the capability must render or interact as a native UI element.
- Use an app extension for widgets, notification services, share actions, or other processes the operating system launches outside the main app.
Keep platform differences behind one typed interface when their product meaning is the same. Expose platform-specific behavior explicitly when pretending it is universal would be misleading.
Design the Contract Before the Implementation
- Define serializable inputs and outputs.
- Model failures with stable error codes and useful messages.
- Decide threading and lifecycle ownership.
- Unsubscribe events and release native resources.
- Avoid blocking synchronous functions.
- Validate availability by OS and version.
Create a minimal example screen and automated contract tests. Native implementation details can change without forcing feature code to change.
Treat Extensions as Separate Processes
Widgets and notification extensions have different memory, timing, entitlement, and lifecycle constraints. Share data through supported app-group storage or platform mechanisms, keep work bounded, and verify signing for every target.
Extension changes require a new native build. They cannot be delivered only through an OTA JavaScript update.
Sources
Use Expo's current documentation for the Modules API, creating a native module, native views, and app extensions when implementing these boundaries.