Passing Data Between Screens in React Navigation Without Shooting Yourself in the Foot
React Navigation route params are stored in navigation state, so what you pass matters. Learn why passing IDs instead of objects keeps deep links, state restoration, and TypeScript working for you.
12 Apr 2026, 11:20 UTC

Every React Native app eventually hits the same moment: a user taps an item in a list, and the detail screen needs to know which item was tapped. React Navigation's answer is route parameters — but the difference between passing an ID and passing an entire object is the difference between an app that survives process death and one that shows stale or broken data.
The short version: pass small, serializable identifiers through route.params, and look everything else up on the destination screen. Here's why, and how to set it up so TypeScript keeps you honest.
How stack navigation actually moves data
A stack navigator works like a deck of cards. navigation.navigate('Details', {...}) pushes a new card on top; the hardware back button (or a swipe on iOS) pops it off. The second argument to navigate becomes route.params on the destination screen.
That params object is stored in the navigation state. This matters because navigation state can be serialized and restored — for example, when Android kills your app in the background and the user returns via the recents screen. If your params contain a class instance, a function, or a 2MB API response, state restoration either fails silently or resurrects stale data.
A typed example that scales
Assuming React Navigation v6 with @react-navigation/native-stack, define a param list once and let it flow through both screens:
// types.ts
export type RootStackParamList = {
Feed: undefined;
ArticleDetail: { articleId: string };
};
// FeedScreen.tsx
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
type Props = NativeStackScreenProps<RootStackParamList, 'Feed'>;
export function FeedScreen({ navigation }: Props) {
return (
<Button
title="Open article"
onPress={() => navigation.navigate('ArticleDetail', { articleId: 'a-1042' })}
/>
);
}
// ArticleDetailScreen.tsx
type DetailProps = NativeStackScreenProps<RootStackParamList, 'ArticleDetail'>;
export function ArticleDetailScreen({ route }: DetailProps) {
const { articleId } = route.params;
// Fetch or select the article by ID here
}With this setup, misspelling articleId or passing a number instead of a string fails at compile time, not in a crash report. If you're in a plain JavaScript project you lose that safety net, which is one more reason to keep params tiny and validate them on arrival.
The ID-instead-of-object rule
The tempting shortcut is navigate('ArticleDetail', { article }) where article is the full object from your list query. It works in the happy path, then bites you in three ways:
- Staleness: the detail screen shows the object as it was at tap time, even if the underlying data changed.
- Serialization: non-plain objects (Dates, class instances, functions) can break state persistence and deep linking.
- Deep links: a URL like
myapp://article/a-1042can only carry strings — if your screen expects an object, deep linking that screen becomes impossible without a rewrite.
Pass the ID, then fetch from your cache or store (React Query, Redux, a context — whatever you already use) on the detail screen. The screen becomes reachable from a push of a button, a deep link, or a notification with zero special-casing.
Reaching navigation from deep in the tree
Drilling navigation through five layers of components is noisy. The useNavigation and useRoute hooks let any component inside a screen access the same objects:
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
function ShareButton({ articleId }: { articleId: string }) {
const nav = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
// nav.navigate('ArticleDetail', { articleId }) works here too
}One caveat: useNavigation returns the nearest navigator's object. If you nest navigators (a stack inside tabs inside a drawer), a component may grab the inner stack when you wanted the outer one. Prefer passing params and callbacks explicitly in deeply nested spots, and keep nesting shallow — each extra navigator layer adds state complexity and can hurt performance.
Trade-offs and how to verify
The ID-based pattern costs you one extra lookup per screen open, and it requires a data layer that can resolve IDs synchronously or show a loading state. For most apps that's a trivial price; for a screen that must render instantly offline, you may pass a small serializable summary (title, thumbnail URL) as a placeholder while the full record loads.
To check your implementation:
- Navigate to the detail screen and confirm
route.params.articleIdmatches the tapped item (a temporaryconsole.logor React DevTools inspection works). - On Android, press the hardware back button and confirm the stack pops to the list rather than exiting the app.
- If you use state persistence, background the app, force-stop it, relaunch, and confirm the detail screen restores correctly — this is where non-serializable params reveal themselves.
The actionable takeaway: treat route params like a URL, not a message bus. Pass identifiers, resolve data at the destination, and type your param list so the compiler enforces the contract. Your deep links, state restoration, and future self will all thank you.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.