Linking subscribe listener falls back to resetRoot() instead of dispatching, unlike linkTo()

Author: MILLERMARRUCreated Aug 13, 2026Updated Sep 17, 2026
Labelsneeds reprostale

Description

Related to #13147, which got auto-closed by the repro bot for lacking a code repro (only screen recordings were provided). I read through useLinking.native.tsx/useLinkTo.tsx and found the actual mechanism, so filing this with that instead of just a video.

The linking config's subscribe listener and linkTo() take structurally different paths when handling a URL:

useLinkTo (used by linkTo()) always builds a targeted action and calls navigation.dispatch(action), unconditionally:

typescript
const linkTo = React.useCallback(
  (href: string) => {
    const action = buildAction(href)
    navigation.dispatch(action)
  },
  [buildAction, navigation],
)

The subscribe listener in useLinking.native.tsx also tries to build an action first, but falls back to a full resetRoot() when it can't:

typescript
if (navigation && state) {
  const action = getActionFromStateRef.current(state, configRef.current)
  if (action !== undefined) {
    navigation.dispatch(action)
  } else {
    navigation.resetRoot(state)
  }
}

resetRoot replaces the entire navigation tree with the state computed purely from the URL, with no knowledge of anything else that happens to be open (other modals, nested navigators, etc). dispatch never does that. So a deep link that arrives through a native Linking event (push notification, universal link, Linking.openURL) can behave very differently from calling linkTo() with the exact same URL, depending only on whether getActionFromState happened to succeed at diffing the current state against the target state.

Reproduction

  1. Open a modal screen A.
  2. Open another modal screen B on top of it.
  3. Trigger a deep link (via the linking config's subscribe, e.g. Linking.openURL(...)) to a screen elsewhere in the app.
  4. Compare against calling linkTo() with the same target path from the same starting state.

With two modals stacked, getActionFromState can't express the target as an incremental diff from the current state (see getActionFromState.tsx in @react-navigation/core), so it returns undefined and the listener falls back to resetRoot, closing modal B (and sometimes leaving things in an inconsistent state depending on gesture/navigation timing). linkTo() on the same URL just dispatches a navigate action and leaves both modals untouched.

Screen recordings showing the modal-closing behavior are in #13147.

Expected behavior

I'd expect the subscribe path to only fall back to a full resetRoot in situations where there's genuinely no navigator alive yet (e.g. cold start deep link), not as a routine fallback whenever the diff algorithm can't find an incremental action while the app is already running with an arbitrary stack of modals open. If that fallback is intentional as a "last resort" for the running-app case too, it'd be worth documenting the behavioral difference from linkTo() explicitly, since right now they look like they should be equivalent and aren't.

Workaround

For anyone hitting this: building and dispatching the action manually inside a custom subscribe handler (the same way useLinkTo does) avoids the fallback entirely, since you control the action-building instead of routing through the built-in listener resolution.

Source: react-navigation/react-navigation