skip to content

With React Navigation 7, how does a screen return a result to its opener, and how do setParams, replaceParams and merge differ?

level: middleimportance: should knowfreq 50%

answer

  1. results travel as params
  2. popTo back to the opener
  3. merge option defaults to false
  4. setParams shallow-merges
  5. replaceParams swaps the whole object

basics

~20 s

Return a result by navigating back with params, for example popTo('Tracking', { orderId, note }, { merge: true }). setParams shallow-merges into the current route's params, replaceParams replaces them, and merge: true keeps a target's existing params.

solid answer

~40 s

React Navigation 7 has no return value from a navigation call, so a result travels as params on the screen you go back to. If Tracking opened a DeliveryNote screen, saving calls `navigation.popTo('Tracking', { orderId, note }, { merge: true })`; Tracking reads `route.params.note` and reacts in an effect keyed on it. By default `popTo` and `navigate` **replace** the target's params, while `{ merge: true }` keeps its other keys and overwrites only those passed. For a screen's own params, `setParams` shallow-merges a partial object into the current route, and `replaceParams` swaps the whole object, dropping keys you leave out. Avoid passing an `onSave` callback in params: it is not serializable, triggers a development warning and breaks state persistence and deep links.

code

tsx · 45 lines
tsx
import { useEffect } from 'react';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import { Button, Text, View } from 'react-native';

type RootStackParamList = {
  Tracking: { orderId: string; tab?: 'map' | 'list'; note?: string };
  DeliveryNote: { orderId: string };
};

export function DeliveryNoteScreen({
  navigation,
  route,
}: NativeStackScreenProps<RootStackParamList, 'DeliveryNote'>) {
  const save = () =>
    navigation.popTo(
      'Tracking',
      { orderId: route.params.orderId, note: 'Leave at the door' },
      { merge: true }
    );
  return <Button title="Save note" onPress={save} />;
}

export function TrackingScreen({
  navigation,
  route,
}: NativeStackScreenProps<RootStackParamList, 'Tracking'>) {
  const { orderId, note } = route.params;

  useEffect(() => {
    if (note !== undefined) {
      console.log(`save note for ${orderId}: ${note}`);
    }
  }, [orderId, note]);

  return (
    <View>
      <Text>Note: {note ?? 'none'}</Text>
      <Button
        title="Add note"
        onPress={() => navigation.navigate('DeliveryNote', { orderId })}
      />
      <Button title="List view" onPress={() => navigation.setParams({ tab: 'list' })} />
    </View>
  );
}

go deeper

for a junior

Recall that results go back as params: the child navigates back to the opener with a param, and the opener reads it from route.params.

for a middle

Explain why v7 uses popTo rather than navigate for this, what merge: true changes, and the shallow merge of setParams against the full swap of replaceParams.

for a senior

Show you keep results small and serializable, react to them in an effect keyed on the value, and move shared entity updates to the data layer instead of shuttling them through params.

for a principal

Decide where cross-screen results belong: params for screen-scoped choices that must survive restoration and links, a shared store for data several features observe.

## Why a result is a param, not a return value In **React Navigation 7**, every navigation call (`navigate`, `push`, `popTo`, `goBack`) dispatches an **action** that changes the navigation state. None of them returns a promise or a value, and `goBack()` takes no arguments. A screen that needs to hand something back to the screen that opened it therefore does what every other screen does: it navigates, and it carries the data in **params**. Take a delivery app where the `Tracking` screen for order A123 opens a `DeliveryNote` screen so the customer can leave instructions for the courier. When the customer taps Save, the note has to reach `Tracking`. ## Returning with popTo or navigate The usual pattern since v7: 1. The child calls `navigation.popTo('Tracking', { orderId, note }, { merge: true })`. 2. The stack router finds the nearest `Tracking` route, removes `DeliveryNote` above it and updates that route's params. 3. `Tracking` re-renders with the new `route.params.note` and reacts in an effect whose dependency is the note, for example by saving it to the order. In React Navigation 6 the same pattern used `navigate`, because `navigate` went back to an existing screen. In v7 `navigate` pushes instead, so the equivalent is `popTo`, or `navigate(name, params, { pop: true })`. The **`merge`** option matters here. By default the params you pass **replace** the target route's params (with its `initialParams` still merged underneath). If `Tracking` also held a `tab: 'map'` param, a replace without it would drop the tab. With `{ merge: true }` the router spreads the old params and then the new ones, so `tab` survives and only `orderId` and `note` are overwritten. ## Returning past several screens `popTo` is not limited to the previous screen. It goes back to the **nearest earlier route** with the given name, removing every route above it in one step. That fits multi-step sub-flows: `Tracking → AddressSearch → AddressConfirm`, where the confirm screen calls `popTo('Tracking', { orderId, addressId }, { merge: true })` and both address screens leave the stack together. If no `Tracking` route exists below, for example because the flow was opened from a deep link, `popTo` removes the current screen and adds `Tracking` in its place instead of failing, so the result still lands. ## Changing the current screen's own params Two methods change the params of the route that calls them, without navigating: | Method | What it does | Keys you omit | |---|---|---| | `navigation.setParams(partial)` | shallow-merges the partial object into the current params | keep their old values | | `navigation.replaceParams(full)` | sets the params to exactly the object passed | are removed | | `navigate` or `popTo` without `merge` | sets the target's params to the object passed, over its `initialParams` | are removed | | `navigate` or `popTo` with `{ merge: true }` | spreads old params, then new ones | keep their old values | Typical uses of the first two: - the tracking screen records the selected tab with `setParams({ tab: 'list' })`, so restored state and a shared link reopen the same tab; - a screen resets a filter set with `replaceParams`, so no stale key lingers from the previous filter; - to clear a single key with `setParams`, set it to `undefined` explicitly, because omitting it keeps the old value. `replaceParams` is the newer of the two; it arrived as a `REPLACE_PARAMS` action in `@react-navigation/core` 7.10.0. Both are also available as action creators, `CommonActions.setParams` and `CommonActions.replaceParams`, for code that dispatches actions directly. ## What not to do - **Callbacks in params.** Passing `onSave` to the child and calling it looks convenient, but a function in the navigation state triggers the development warning about non-serializable values, cannot be persisted or restored, and cannot come from a deep link. It also closes over the opener's state at the moment of navigation, which may be stale by the time it runs. - **Expecting `goBack` to carry data.** It has no parameters; use `popTo` with params. - **Reading the result once on mount.** The opener was already mounted when the result arrived, so a one-time read on mount misses it; read `route.params` during render or in an effect keyed on the value. ## When params are the wrong channel Params suit small, screen-scoped results: a chosen slot, a note, a selected address id. When the result is really application data that other screens also show, such as the order itself being updated on the server, write it to the app's data layer and let every screen read from there; the returning screen then only needs to go back. Mixing the two, a full entity copied into params, recreates the staleness problem that passing ids avoids.

  • Why does the opener react in an effect keyed on the param instead of reading it on mount?
    In a stack the opener was already rendered before the child opened, so a read that runs only once on mount has already happened. The result arrives as a params update on the existing route, which re-renders the screen; an effect whose dependency is `route.params.note` runs exactly when a new value lands.
  • When would you choose replaceParams over setParams?
    When the new params must be the whole truth, such as switching between filter presets where a key from the previous preset must not linger. `setParams` shallow-merges, so an omitted key keeps its old value and must be set to `undefined` to clear it; `replaceParams` drops anything not passed.

saying these in an interview costs you the question

  • Pass an onSave callback in params and have the child call it.
  • navigation.goBack(result) hands a value back to the previous screen.
  • setParams replaces the whole params object, dropping keys you omit.
  • popTo merges new params into the target route by default.
  • navigate returns a promise that resolves with the child screen's result.