Why Parameters and Navigation State Matter
Once you have multiple screens wired up with a stack or tabs, the next practical challenge is moving data between those screens and keeping the navigation experience consistent. “Passing parameters” means sending small pieces of data (IDs, titles, filters, flags) along with a navigation action so the destination screen can render the right content. “Managing navigation state” means understanding and controlling the current route history, the active screen, and how the back stack behaves when users move around your app.
In real apps, you rarely navigate to a screen without context. You open a product details screen for a specific product, a profile screen for a specific user, or a checkout screen with a cart total. At the same time, you want predictable behavior when users press Back, when they deep link into a screen, or when you reset the stack after login.
Passing Parameters Between Screens
What counts as a “parameter”?
In React Navigation, parameters are typically stored on a route object and are accessible on the destination screen. Parameters should be serializable (strings, numbers, booleans, plain objects/arrays). Avoid passing functions, class instances, or huge objects. Treat params as “navigation context,” not as a replacement for app state management.
- Good params:
productId,userId,initialTab,sort,from - Risky params: entire product objects with images and nested relations, non-serializable values (functions, Dates without conversion)
Step-by-step: Navigate with params (Stack)
Assume you have a list screen that navigates to a details screen. The list screen passes an ID and maybe a title for the header.
// ProductListScreen.js (or .tsx) function ProductListScreen({ navigation }) { const products = [ { id: 'p1', name: 'Coffee Beans' }, { id: 'p2', name: 'Tea Sampler' }, ]; return ( <> {products.map((p) => ( <Pressable key={p.id} onPress={() => navigation.navigate('ProductDetails', { productId: p.id, title: p.name, from: 'ProductList', }) } > <Text>{p.name}</Text> </Pressable> ))} </> ); }On the destination screen, you read the params from route.params.
- Listen to the audio with the screen off.
- Earn a certificate upon completion.
- Over 5000 courses for you to explore!
Download the app
// ProductDetailsScreen.js function ProductDetailsScreen({ route, navigation }) { const { productId, title, from } = route.params ?? {}; // Use productId to fetch data or select from store // Use title to set the header, if desired return ( <View> <Text>Product ID: {productId}</Text> <Text>Came from: {from}</Text> </View> ); }Notice the defensive route.params ?? {}. This helps avoid crashes if the screen is opened without params (for example, via a deep link or a reset action). In production, you should decide what to do when required params are missing: show an error state, redirect back, or load a default.
Setting screen options based on params
A common pattern is to set the header title based on a param. You can do this in a few ways. One practical approach is to set options inside the screen using useLayoutEffect so it updates quickly.
import { useLayoutEffect } from 'react'; function ProductDetailsScreen({ route, navigation }) { const { title } = route.params ?? {}; useLayoutEffect(() => { if (title) { navigation.setOptions({ title }); } }, [navigation, title]); return ( <View> <Text>Details...</Text> </View> ); }Keep the header title param small and stable. If the title can change after fetching data, you can update options once the data arrives.
Updating params after navigation
Params are not only for the initial navigation. You can update them later using navigation.setParams. This is useful when a screen’s internal state should be reflected in the route (for example, a filter applied on a search screen).
function SearchScreen({ route, navigation }) { const { sort = 'relevance' } = route.params ?? {}; function applySort(nextSort) { navigation.setParams({ sort: nextSort }); } return ( <View> <Text>Current sort: {sort}</Text> <Button title="Sort by price" onPress={() => applySort('price')} /> </View> ); }Be careful not to overuse this. If you find yourself storing lots of UI state in params, consider whether it belongs in component state or a shared store instead. Params are best for “how you got here” and “what this screen should show.”
Sending data back to a previous screen
Sometimes you navigate to a screen to pick something (a contact, an address, a color) and then return the selection. There are multiple ways to do this. A simple approach is: navigate back and update the previous screen via params or a shared store. Another approach is to pass a callback, but callbacks are not serializable and can cause issues with state persistence and debugging.
A practical, serializable pattern is to navigate back with a value stored in params on the previous route. One way is to update the previous screen’s params before going back.
// AddressPickerScreen function AddressPickerScreen({ navigation }) { function chooseAddress(addressId) { // Update the previous screen's params, then go back navigation.navigate({ name: 'Checkout', params: { selectedAddressId: addressId }, merge: true, }); navigation.goBack(); } return ( <View> <Button title="Use Address A1" onPress={() => chooseAddress('a1')} /> </View> ); }On the Checkout screen, read route.params.selectedAddressId and react to changes. The merge: true option helps preserve existing params rather than replacing them entirely.
If your app has complex “return values” (multiple fields, validation, persistence), it’s often cleaner to store the selection in a shared state (context/store) and simply navigate back.
Params in nested navigators (Tabs inside Stack, etc.)
When you have nested navigators, you may need to target a specific screen inside a nested navigator and pass params to it. The general idea is: navigate to the parent route and specify the nested screen and its params.
// Example: Stack has a route named "MainTabs" which contains a tab screen "Settings" navigation.navigate('MainTabs', { screen: 'Settings', params: { highlight: 'privacy' }, });On the Settings screen, you still read params from route.params. The key is that the params are attached to the nested screen route, not the parent.
Managing Navigation State
What is navigation state?
Navigation state is the data structure that describes where the user is in your app: which navigator is active, which route is focused, and the history stack (for stack navigators). React Navigation manages this for you, but you often need to influence it to achieve common product behaviors:
- Prevent returning to login after successful authentication
- Clear intermediate screens after completing a flow (e.g., checkout)
- Ensure a tab opens to a specific nested screen
- Handle the Android hardware back button in a controlled way
- React to route focus/blur to refresh data or stop timers
Inspecting the current route and state
To make decisions, you need to know what screen is focused. You can use hooks like useRoute and useNavigation for local access, and useNavigationState to read the current state.
import { useNavigationState } from '@react-navigation/native'; function DebugCurrentRoute() { const routeName = useNavigationState((state) => { const route = state.routes[state.index]; return route?.name; }); return <Text>Current route: {routeName}</Text>; }This is helpful for debugging and for conditional UI (for example, hiding a floating button on certain screens). Avoid building heavy logic that depends on constantly reading navigation state; prefer explicit props/state where possible.
Reacting to focus: refresh on screen focus
When a user navigates back to a screen, you may want to refresh data. Instead of relying on component mount/unmount, use focus events. React Navigation provides useFocusEffect for this.
import { useCallback } from 'react'; import { useFocusEffect } from '@react-navigation/native'; function OrdersScreen() { useFocusEffect( useCallback(() => { // Fetch latest orders when screen becomes focused // Return a cleanup function if needed return () => {}; }, []) ); return ( <View> <Text>Orders</Text> </View> ); }This pattern is especially useful when a details screen can modify something and you want the list screen to reflect changes when the user returns.
Controlling the back stack: replace vs navigate
When you call navigate, you typically push a new screen onto the stack (or jump to an existing route depending on configuration). Sometimes you want to replace the current screen so the user can’t go back to it. That’s where replace is useful.
// After a successful form submission, replace the form with a success screen navigation.replace('Success', { orderId: 'o123' });Use replace when the previous screen should not remain in history (e.g., a one-time onboarding step, a payment screen after completion).
Resetting navigation state for auth flows
A very common requirement is: after login, the user should not be able to press Back and return to the login screen. The clean solution is to reset the navigation state so the authenticated area becomes the new root.
import { CommonActions } from '@react-navigation/native'; function LoginScreen({ navigation }) { function onLoginSuccess() { navigation.dispatch( CommonActions.reset({ index: 0, routes: [{ name: 'MainTabs' }], }) ); } return <Button title="Log in" onPress={onLoginSuccess} />; }reset replaces the entire state with the routes you provide. This is also useful after completing a multi-step flow where you want to land on a final screen and remove the intermediate steps from history.
Pop actions: going back multiple screens
Sometimes you need to jump back more than one screen (for example, from a confirmation screen back to the start of a flow). You can pop screens from the stack.
// Go back one screen navigation.goBack(); // Go back two screens navigation.pop(2); // Go back to the first screen in the stack navigation.popToTop();Use these when you know the user arrived through a stack sequence. If the user can arrive from multiple entry points, a reset to a known route is often safer than popping an assumed number of screens.
Preventing accidental exits and intercepting back behavior
There are cases where you want to prevent leaving a screen (unsaved changes, in-progress payment). React Navigation can block navigation actions using the beforeRemove event. This works for header back, gestures, and Android hardware back.
import { useEffect, useState } from 'react'; function EditProfileScreen({ navigation }) { const [isDirty, setIsDirty] = useState(false); useEffect(() => { const unsubscribe = navigation.addListener('beforeRemove', (e) => { if (!isDirty) return; // Prevent default behavior of leaving the screen e.preventDefault(); // Show your own confirmation UI (simple example) // If user confirms, dispatch the original action // navigation.dispatch(e.data.action); }); return unsubscribe; }, [navigation, isDirty]); return ( <View> <Text>Edit Profile</Text> <Button title="Make change" onPress={() => setIsDirty(true)} /> </View> ); }The key idea: you intercept the removal event, decide whether to allow it, and if allowed, dispatch the original action. In a real app, you’d show a modal confirmation and only dispatch if the user confirms.
Deep linking and missing params: designing resilient screens
When screens can be opened from outside the app (deep links, push notifications) or from multiple places inside the app, you must assume params might be missing or partial. A resilient strategy:
- Define which params are required (e.g.,
productId) and which are optional (e.g.,title). - If required params are missing, render an error state or redirect to a safe screen.
- Prefer passing IDs and fetching the latest data rather than passing full objects.
function ProductDetailsScreen({ route, navigation }) { const productId = route.params?.productId; useEffect(() => { if (!productId) { // Redirect to a safe place if opened incorrectly navigation.replace('ProductList'); } }, [productId, navigation]); if (!productId) return null; return <Text>Loading product {productId}...</Text>; }This approach prevents crashes and keeps navigation state consistent even when entry points vary.
Practical Mini-Flow: List → Details → Edit → Back with Updated Data
Goal
You will implement a common pattern: a list screen opens details for an item, details opens an edit screen, and when the user saves, you return to details and reflect the updated value. The focus is on params and navigation state behavior, not on global state libraries.
Step 1: Navigate from List to Details with an ID
// ListScreen function ListScreen({ navigation }) { const items = [ { id: '1', name: 'First' }, { id: '2', name: 'Second' }, ]; return ( <View> {items.map((item) => ( <Pressable key={item.id} onPress={() => navigation.navigate('Details', { itemId: item.id })} > <Text>{item.name}</Text> </Pressable> ))} </View> ); }Step 2: In Details, open Edit and pass current values
Details reads itemId and also listens for an optional updatedName param that might be set when returning from Edit.
// DetailsScreen function DetailsScreen({ route, navigation }) { const { itemId, updatedName } = route.params ?? {}; const nameToShow = updatedName ?? `Item ${itemId}`; return ( <View> <Text>Details for {itemId}</Text> <Text>Name: {nameToShow}</Text> <Button title="Edit name" onPress={() => navigation.navigate('Edit', { itemId, currentName: nameToShow, }) } /> </View> ); }Step 3: In Edit, save and update Details params, then go back
Instead of passing a callback, Edit updates the Details route params (merging) and then returns. This keeps the data serializable and works well with state persistence.
// EditScreen import { useState } from 'react'; function EditScreen({ route, navigation }) { const { itemId, currentName } = route.params ?? {}; const [name, setName] = useState(currentName ?? ''); function save() { navigation.navigate({ name: 'Details', params: { itemId, updatedName: name }, merge: true, }); navigation.goBack(); } return ( <View> <Text>Editing {itemId}</Text> <TextInput value={name} onChangeText={setName} /> <Button title="Save" onPress={save} /> </View> ); }This mini-flow demonstrates a practical rule: use params to communicate navigation context and small updates, and use navigation actions (replace, reset, pop) to shape the back stack so users can’t return to screens that no longer make sense.
Common Pitfalls and How to Avoid Them
Passing large objects and stale data
If you pass a full object to a details screen, it may become stale if the data changes elsewhere. Prefer passing an ID and fetching/selecting the latest data. If you do pass an object for performance or convenience, treat it as an initial snapshot and still be able to refresh.
Non-serializable params
Avoid passing functions, navigation objects, or complex instances. Non-serializable values can break state persistence and make debugging harder. If you need to trigger behavior on return, use focus events or shared state rather than callbacks in params.
Assuming a fixed back stack
Users can reach screens from different paths (tabs, notifications, deep links). If you need to guarantee where the user lands after an action, use reset or replace to enforce a known state rather than popping an assumed number of screens.
Not handling missing required params
Screens should validate required params and handle missing values gracefully. This is especially important once you add deep links or external entry points.