---
name: react-native-navigation
description: >-
  Type-safe React Navigation patterns: param list design, deep linking that
  survives cold start, and keeping navigation out of domain hooks. Use when
  adding screens, routes, or deep links.
argument-hint: "[screen name]"
license: MIT
metadata:
  author: John Felix Lim
  version: "1.0.0"
---
# Navigation

Apply when adding a screen, changing route params, or wiring a deep link. Treat any screen reachable by URL as if it will always be cold-started — because for some users it always is.

## Rules

1. **Params carry identifiers, not entities** — Pass a `userId`, not a user object. Params get serialized into deep links and restored state; an object that was fresh when pushed is stale when restored, and it silently bloats the navigation state.
2. **The param list is the source of truth** — Declare one `RootStackParamList` and let TypeScript reject every wrong `navigate` call at compile time. Untyped navigation is the most common source of runtime crashes in a large RN app.
3. **Design deep links for a cold start** — A link opened from a killed app has no navigation history. If your screen assumes a previous route existed — for a back button, or for data it expected to be prefetched — it will crash for exactly the users arriving from a marketing campaign.
4. **Keep navigation out of the domain layer** — A hook that both fetches data and calls `navigation.navigate` cannot be tested or reused. Return a result; let the screen decide where to go.

## Patterns

### Typed params

**Avoid** — An `as any` cast at the boundary where the compiler could have helped most. Renaming the route breaks nothing at build time and everything at runtime.

```tsx
// No param list — every navigate call is a guess
navigation.navigate("ProductDetail", { product });

// In ProductDetail:
const { product } = route.params as any;
```

**Prefer** — The global augmentation types every `useNavigation()` call in the app without per-call generics.

```tsx
export type RootStackParamList = {
  Home: undefined;
  ProductDetail: { productId: string };
};

declare global {
  namespace ReactNavigation {
    interface RootParamList extends RootStackParamList {}
  }
}

// Wrong route name or wrong params is now a compile error.
navigation.navigate("ProductDetail", { productId: product.id });
```

### Cold-start-safe screens

**Avoid** — Works when navigated from the list, crashes when opened from a push notification.

```tsx
function ProductDetail({ route }) {
  const { product } = route.params;
  // Deep link into a killed app: params were serialized,
  // `product` is a stale snapshot or missing entirely.
  return <Text>{product.name}</Text>;
}
```

**Prefer** — The screen fetches from an id, so it behaves identically whether it was pushed or cold-started from a link.

```tsx
function ProductDetail({ route }: Props) {
  const { productId } = route.params;
  const { data, isPending, error } = useProduct(productId);

  if (isPending) return <ProductDetailSkeleton />;
  if (error) return <ErrorState onRetry={refetch} />;

  return <Text>{data.name}</Text>;
}
```

### Navigation stays in the UI layer

**Avoid** — The hook now depends on a navigator. It cannot be unit tested or reused from a different screen.

```ts
export function useSubmitOrder() {
  const navigation = useNavigation();
  return useMutation({
    mutationFn: postOrder,
    onSuccess: (order) => navigation.navigate("Receipt", { id: order.id }),
  });
}
```

**Prefer** — The hook returns a result. The screen owns the routing decision.

```ts
export function useSubmitOrder() {
  return useMutation({ mutationFn: postOrder });
}

// In the screen — the only place that knows where to go next:
const { mutate } = useSubmitOrder();
mutate(draft, {
  onSuccess: (order) => navigation.navigate("Receipt", { id: order.id }),
});
```

## Review checklist

- [ ] A single param list is declared and globally augmented.
- [ ] No screen param carries a full entity — ids only.
- [ ] Every deep-linkable screen renders correctly from a cold start.
- [ ] Deep-linked screens handle their own loading and error states.
- [ ] No hook outside the UI layer imports `useNavigation`.
- [ ] Back behaviour is verified on Android hardware back, not just the header button.

---

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