skip to content

With React Navigation 7, why would you override linking.getInitialURL and linking.subscribe so a tapped push notification opens a product screen?

level: seniorimportance: should knowfreq 30%

answer

  1. defaults only know Linking
  2. a notification tap is not a URL event
  3. cold start: getInitialURL, warm: subscribe
  4. subscribe returns its cleanup
  5. fallback shows while it resolves

basics

~20 s

React Navigation 7's defaults read URLs only from React Native's Linking. A push tap delivers its URL through the notification library instead, so getInitialURL must also check the launch notification and subscribe must also listen for taps.

solid answer

~40 s

By default, `getInitialURL` asks `Linking.getInitialURL()` for the launch URL and `subscribe` listens to `Linking`'s `'url'` event. A push notification for a sofa back in stock typically carries the URL in its data, and a tap on it is reported by the push library, not by `Linking`. So I override both: `getInitialURL` returns the `Linking` URL if there is one, otherwise the URL from the notification that launched the app; `subscribe` registers both a `Linking` listener and a notification-tap listener, passes each URL to React Navigation's listener, and returns a cleanup removing both. The rest of the pipeline — prefixes, `getStateFromPath`, nested config — is unchanged. While an async `getInitialURL` is pending, `NavigationContainer` renders its `fallback`, so it must always settle.

code

typescript · 29 lines
typescript
import { Linking } from 'react-native';
import type { LinkingOptions } from '@react-navigation/native';
import { addNotificationTapListener, getLaunchNotificationUrl } from './push';
import type { RootStackParamList } from './types';

const withTimeout = <T,>(promise: Promise<T>, ms: number, fallback: T) =>
  Promise.race([promise, new Promise<T>((resolve) => setTimeout(() => resolve(fallback), ms))]);

export const linking: LinkingOptions<RootStackParamList> = {
  prefixes: ['furnistore://', 'https://shop.example.com'],
  config: { screens: { Product: 'product/:sku' } },
  async getInitialURL() {
    try {
      const url = await Linking.getInitialURL();
      if (url != null) return url;
      return await withTimeout(getLaunchNotificationUrl(), 500, null);
    } catch {
      return null;
    }
  },
  subscribe(listener) {
    const linkSubscription = Linking.addEventListener('url', ({ url }) => listener(url));
    const removeTapListener = addNotificationTapListener((url) => listener(url));
    return () => {
      linkSubscription.remove();
      removeTapListener();
    };
  },
};

go deeper

for a junior

Know that React Navigation reads launch and incoming URLs through getInitialURL and subscribe, and that both default to React Native's Linking.

for a middle

Explain why a notification tap is invisible to the defaults on cold and warm start, and what each override must return.

for a senior

Guard the async getInitialURL with a timeout and error path, clean up both listeners, and remember initialState disables linking on the first render.

for a principal

Unify every entry point — links, notifications, widgets — behind one URL contract so routing logic lives in a single configuration.

## What the defaults do **React Navigation 7** gets URLs from two functions on the `linking` options, and both have defaults built on React Native's `Linking` module: | Option | Default | Used for | |---|---|---| | `getInitialURL` | `Linking.getInitialURL()`, raced against a 150 ms timeout | The URL that launched the app (cold start) | | `subscribe` | `Linking.addEventListener('url', …)`, returning a cleanup | URLs that arrive while the app runs (warm) | The timeout in the default `getInitialURL` is a workaround for a React Native issue where the launch URL promise could fail to resolve; it means a missing launch URL never blocks startup for long. ## Why notifications do not fit the defaults A furniture store sends "The oak sofa is back in stock". Tapping the notification should open `Product` for that `sku`. The notification's payload usually carries a URL or path, but the **tap is reported by the push notification library**, not by `Linking`: - On a **cold start**, `Linking.getInitialURL()` returns `null`, because the app was not opened by a link. - On a **warm start**, no `'url'` event fires. Without overrides, the app opens on its home screen and the customer has to find the sofa again. ## The overrides 1. **`getInitialURL`** — may be async. First await `Linking.getInitialURL()`; if it returns a URL, use it (a link still wins). Otherwise ask the push library whether the app was launched from a notification tap and return the URL in that notification's data, or `null`. 2. **`subscribe(listener)`** — register a `Linking` `'url'` listener **and** a notification-tap listener. Each calls `listener(url)`. Return a function that removes both, because React Navigation calls it when the container unmounts or the subscription is replaced. After that, the URL flows through the same pipeline as a normal link: prefix stripping, `filter`, `getStateFromPath` against `config.screens`, then initial state (cold) or a dispatched action (warm). The notification URL must therefore use one of the configured `prefixes`. ## Things that go wrong - **A `getInitialURL` that never settles.** While it is pending, `NavigationContainer` renders its **`fallback`** prop (default `null`, so a blank screen). Your override does not inherit the default's 150 ms race; a push-library call that hangs leaves the customer staring at nothing. Add your own timeout, and catch errors so they resolve to `null`. - **A leaked listener.** Returning nothing from `subscribe`, or removing only the `Linking` listener, keeps a stale notification listener alive after a remount and can navigate twice. - **Using the removed API.** Older examples return `() => Linking.removeEventListener('url', handler)`; current React Native returns a subscription from `addEventListener`, and the cleanup calls its `remove()`. - **An `initialState` prop.** If the container is given `initialState` (for example restored persisted state), linking is not used for the initial render at all, so the launch notification is ignored. ## Validating what arrives A notification payload is data from a server, and on some platforms other apps or users can craft URLs to your scheme. Treat the `sku` from a notification like any linked param: the product screen must handle unknown values, and the notification's URL should be checked against your own prefixes before use. ## Testing the overrides 1. Mock `Linking.getInitialURL()` to return `null` and the push wrapper to return a product URL; assert that `getInitialURL` resolves to that URL. 2. Make the push wrapper never resolve; assert that `getInitialURL` still resolves to `null` after the timeout, so the container leaves its `fallback`. 3. Call `subscribe` with a spy, fire the captured notification-tap callback, and assert the spy receives the URL. 4. Call the returned cleanup and assert both the `Linking` subscription and the tap listener were removed. ## Where the boundary sits Receiving, displaying and permissioning notifications, and delivering raw URLs to the app on cold and warm start, are handled elsewhere. This question is only about teaching React Navigation where URLs come from so that its normal path-to-screen mapping can do the rest.

  • What does the user see while an async getInitialURL is still pending?
    `NavigationContainer` renders its `fallback` prop until the initial state is resolved; the default is `null`, so a blank screen. That is why a custom `getInitialURL` needs a timeout and an error path: unlike the default, it has no built-in race.
  • Why does a notification tap on a warm app not reach React Navigation without the subscribe override?
    The default `subscribe` only listens to `Linking`'s `'url'` event. A notification tap is reported by the push library's own tap listener, so no `'url'` event fires; the override bridges that listener into React Navigation's `listener(url)`.

saying these in an interview costs you the question

  • Linking.getInitialURL() returns the URL from the notification that launched the app.
  • A custom getInitialURL keeps the default's short timeout automatically.
  • subscribe does not need to return anything; React Navigation cleans up itself.
  • Notification URLs bypass prefixes and the screens config.