---
name: react-native-platform
description: >-
  Handle iOS/Android platform differences, safe areas, permission flows, and
  native module decisions in React Native. Use when writing
  platform-specific code or integrating device capabilities.
argument-hint: "[capability]"
license: MIT
metadata:
  author: John Felix Lim
  version: "1.0.0"
---
# Platform & Native

Apply when code needs to differ between platforms or touch a device capability. Default to keeping the difference at the UI edge, and treat permission denial as a designed path rather than an error case.

## Rules

1. **Push platform differences to the edges** — `Platform.OS` scattered through business logic makes every rule twice as hard to read and test. Isolate the difference in a component or a constant, and keep the branch out of the logic.
2. **Prefer `Platform.select` over if-chains** — It is exhaustive, it reads as a table of values rather than a branch, and it collapses to a constant. An `if (Platform.OS === 'ios')` chain invites a third case nobody handles.
3. **Reach for a native module last** — A native module doubles your build surface, blocks Expo Go, and needs someone who can debug Swift and Kotlin at 2am. Exhaust the JS ecosystem first — and when you do write one, own the maintenance honestly.
4. **Safe areas are a layout concern, not a constant** — Hardcoded status bar heights break on every new device notch, on Android gesture navigation, and in landscape. `useSafeAreaInsets` is the only thing that stays correct.
5. **Test permission denial as a first-class path** — Users deny camera and location permissions constantly, and on iOS the denial is permanent until they visit Settings. A flow that only works on grant is a flow that is broken for a real fraction of your users.

## Patterns

### Platform branching

**Avoid** — A branch in logic, plus a fallback that documents nothing.

```ts
function getHeaderHeight() {
  if (Platform.OS === "ios") {
    return 88;
  } else if (Platform.OS === "android") {
    return 56;
  }
  return 64; // web? macOS? Nobody knows.
}
```

**Prefer** — A value table rather than control flow. When implementations diverge entirely, platform file extensions keep both readable.

```ts
const HEADER_HEIGHT = Platform.select({
  ios: 88,
  android: 56,
  default: 64,
}) as number;

// Or push it into the file system when the whole
// implementation differs:
//   Picker.ios.tsx
//   Picker.android.tsx
//   import { Picker } from "./Picker";  // resolved by extension
```

### Safe area handling

**Avoid** — Wrong on every notched device, wrong in landscape, wrong with Android gesture navigation.

```tsx
const STATUS_BAR = Platform.OS === "ios" ? 44 : 24;

<View style={{ paddingTop: STATUS_BAR }}>
```

**Prefer** — Correct on every device and orientation, including hardware that shipped after your app did.

```tsx
const insets = useSafeAreaInsets();

<View style={{ paddingTop: insets.top }}>
```

### Permissions as a real flow

**Avoid** — Silent failure. The user taps again, gets nothing, and leaves a one-star review.

```tsx
const granted = await requestCameraPermission();
if (granted) openCamera();
// Denied? Nothing happens. The button appears broken.
```

**Prefer** — Denial and permanent block are distinct states, and each has a real path forward.

```tsx
const status = await requestCameraPermission();

if (status === "granted") return openCamera();

if (status === "blocked") {
  // iOS will not re-prompt. Settings is the only path.
  return promptOpenSettings();
}

return showRationale();
```

## Review checklist

- [ ] No `Platform.OS` inside domain logic.
- [ ] Divergent implementations use `.ios.tsx` / `.android.tsx` rather than branches.
- [ ] All insets come from `useSafeAreaInsets`.
- [ ] Every permission flow handles granted, denied, and blocked separately.
- [ ] Native modules are justified in writing, with a named maintainer.
- [ ] Tested on a notched iPhone and on Android gesture navigation.

---

From the John Felix Lim Frontend Engineering Playbook — https://github.com/JohnFelixLim