Native Integration Skill
Applicability
- Platforms: iOS and Android
- React Native: 0.76+ (New Architecture interop assumed unless a checklist item says otherwise)
When to Use
- Integrating a native module, Turbo Module, or platform API
- Requesting permissions, or handling camera, biometrics, files, or location
- Adding a background task or a native listener
- Reviewing a change that crosses the JavaScript and native boundary
Severity
- Merge-blocking: a permission prompt this diff adds loops after denial, or a listener this diff adds is not removed on unmount.
- Should-fix: usage-string wording and when the prompt appears, as long as denial does not crash.
Guidance
Permissions
This skill owns when the prompt is shown and what denial looks like. Analytics consent is owned by security.
- Permission is requested at the moment of use, not on app launch or screen mount
- Denied, restricted, and revoked states have a path that does not crash or loop the prompt
- The user can continue without the permission, or is told what is blocked and how to enable it in Settings
- iOS usage strings explain the specific purpose; Android declares only the permissions the feature uses
Incorrect:
useEffect(() => {
requestCameraPermission();
}, []);Correct:
async function onTakePhoto() {
const status = await getCameraPermission();
if (status === 'denied') {
const next = await requestCameraPermission();
if (next !== 'granted') {
setNeedsSettings(true);
return;
}
}
if (status === 'blocked') {
setNeedsSettings(true);
return;
}
await openCamera();
}Native boundary
- The JavaScript API returns a result or a typed error; it does not throw an unstructured native exception into the screen
- A missing or old native implementation has a fallback (hide the feature, or use a supported path)
- Values crossing the bridge are serialisable; functions, class instances, and cyclic objects are not passed through
- Platform differences are explicit (
Platform.OSor separate files), not assumed to match - New Architecture interop is used when the app is on the New Architecture; the old bridge is not added for a new module without a reason
Lifecycle
- Native listeners, sensors, and subscriptions are removed on unmount
- Background work respects OS limits (time, network, and user-visible purpose) and can be cancelled
- Work that must finish (upload, payment) is confirmed on the server, not only by a native callback
- Native UI that presents a modal or activity restores the React Native screen when it dismisses
Anti-Patterns
| Anti-Pattern | Risk | Fix |
|---|---|---|
Requesting permission in useEffect on mount |
Prompt before the user understands why; denial with no recovery | Request from the action that needs it, and handle denial |
| Treating "granted once" as permanent | Revoked or limited permission crashes the next call | Check status before every use |
| Passing a JS callback object into native and never removing it | Leak and calls after unmount | Subscribe with a cleanup function |
Swallowing native errors as catch {} |
The screen looks idle while the feature failed | Surface a typed error and a retry or fallback |
Pitfalls
- Android may deliver a permission result after the activity restarts. Do not assume the component that called
requestis still mounted. - iOS limited photo access is not the same as full access. Handle the limited set instead of treating it as denied or granted.
- Biometric success proves the device unlocked a key; it does not by itself prove the server accepted the user. Confirm the session server-side.
- A native module that touches UI or sensors must run on the platform's main thread. A background-thread call can crash or silently drop the event.
- File URIs from the camera or picker are often temporary. Copy what you must keep, and do not log the path if it contains user content.