---
title: 'Notifications and Deep Linking'
source: 'https://academia.sh/en/courses/react-native/notifications-and-deep-linking'
course: 'React Native'
language: en
updated: '2026-10-21T05:02:54+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Explains how a deep link is converted to a route list'
    - 'Measures that when the app is open, a link mounts only the target screen on the stack'
    - 'Measures that when the app is closed, a link mounts the whole tree'
    - 'Shows that an unrecognized and a duplicate link produce no commands in the tree'
---

# Notifications and Deep Linking

Tapping a notification or opening a link starts the app at a specific screen. The lesson measures that in a running app a link builds a single screen on the stack, while in a closed app it builds the whole tree.

In the previous lessons the change in the tree came from inside the app: a tap, a response, a session. A change also comes from outside. The user taps a notification or clicks a link, and the app has to open on the screen of order 42. This lesson covers how an address that arrives from outside is converted to a route list and what the app being open or closed makes a difference to in the tree.

The question of the course does not change: **what does this update do to the node in the view tree?** The fate is kept, updated, mounted, or unmounted; the total of the updated, mounted, and unmounted nodes is the commands. We do not assign a fate by hand; the rule gives it:

```javascript
// fate.mjs — classifies what an update does to each node of the view tree
export const FATES = ["kept", "updated", "mounted", "unmounted"];

export function el(type, props = {}, children = [], key = null) {
  return { type, props, children, key };
}

function segment(node) {
  return node.key === null ? node.type : `${node.type}[${node.key}]`;
}

function identity(node, index) {
  return node.key === null ? `#${index}` : `key:${node.key}`;
}

function sameProps(a, b) {
  const names = [...new Set([...Object.keys(a), ...Object.keys(b)])].sort();
  return names.every((n) => JSON.stringify(a[n]) === JSON.stringify(b[n]));
}

function whole(node, path, fate, ops) {
  const here = `${path}/${segment(node)}`;
  ops.push({ fate, path: here });
  node.children.forEach((child) => whole(child, here, fate, ops));
}

export function reconcile(prev, next, path = "", ops = []) {
  if (prev === null) {
    whole(next, path, "mounted", ops);
    return ops;
  }
  if (next === null) {
    whole(prev, path, "unmounted", ops);
    return ops;
  }
  if (prev.type !== next.type || prev.key !== next.key) {
    whole(prev, path, "unmounted", ops);
    whole(next, path, "mounted", ops);
    return ops;
  }
  const here = `${path}/${segment(next)}`;
  ops.push({ fate: sameProps(prev.props, next.props) ? "kept" : "updated", path: here });
  const before = new Map(prev.children.map((child, i) => [identity(child, i), child]));
  const matched = new Set();
  next.children.forEach((child, i) => {
    const id = identity(child, i);
    matched.add(id);
    reconcile(before.has(id) ? before.get(id) : null, child, here, ops);
  });
  prev.children.forEach((child, i) => {
    if (!matched.has(identity(child, i))) reconcile(child, null, here, ops);
  });
  return ops;
}

export function printOps(ops) {
  ops.forEach((op) => console.log(`${op.fate}: ${op.path}`));
}

export function countFates(ops) {
  for (const name of FATES) {
    console.log(`${name}: ${ops.filter((op) => op.fate === name).length}`);
  }
  console.log(`commands: ${ops.filter((op) => op.fate !== "kept").length}`);
}
```

The model builds this fate's rule; the model is not the framework itself. The rule does not see where the link came from; it compares only the tree before and the tree after.

## A Link Becomes a Route

A deep link is an address that points to a specific screen of the app. A notification carries this address, a link in an email or another app opens it. A link is not a screen: the address is a piece of text, and the app's route table converts it to a route. The platform delivers the link in two ways. If the app is open, the link reaches the running app as an event. If the app is closed, the link is taken as the first address that starts the app.

```swift
// Platform code: NotificationTap
func userNotificationCenter(_ center: UNUserNotificationCenter,
                            didReceive response: UNNotificationResponse,
                            withCompletionHandler completionHandler: @escaping () -> Void) {
    let link = response.notification.request.content.userInfo["link"] as? String
    pendingLink = link
    completionHandler()
}
```

The platform side catches the link and passes it to the JavaScript side. In the framework there are two entries: asking for the first address at launch and listening for the event that arrives while the app is open.

```jsx
// Platform code: LinkListener
function useLink(onLink) {
  useEffect(() => {
    Linking.getInitialURL().then((url) => url && onLink(url));
    const sub = Linking.addEventListener("url", (e) => onLink(e.url));
    return () => sub.remove();
  }, []);
}
```

These two fragments are not measured; the model does not run the platform or the framework.

**AL1.** The model builds the link as a single address in the form `order/<id>`. `openLink(routes, link)` takes the current route list and the address and returns the new route list. If the address is not recognized, or the target route is already at the top of the stack, the list is returned as it is.

```javascript
// link.mjs — converts a deep link to a route list
export function openLink(routes, link) {
  const parts = link.split("/");
  if (parts[0] !== "order" || parts.length !== 2) return routes;
  const target = "order-" + parts[1];
  if (routes[routes.length - 1] === target) return routes;
  return [...routes, target];
}
```

**AL2.** We build the stack's tree from the routes as in the first lesson: under `Stack`, a keyed `Screen` for each route and a `Text` inside it.

```javascript
// stack.mjs — builds the view tree from the navigation stack's routes
import { el } from "./fate.mjs";

export function renderStack(routes) {
  return el("Stack", {}, routes.map((name) =>
    el("Screen", { title: name }, [el("Text", { text: name })], name)));
}
```

## When the App Is Open

The user went down from the order list to the detail of an order, and then a notification arrived. The stack has `list` and `detail`. When the notification is tapped, the app is already running and the link arrives as an event.

```javascript
// main.mjs
import { reconcile, printOps, countFates } from "./fate.mjs";
import { renderStack } from "./stack.mjs";
import { openLink } from "./link.mjs";

const routes = ["list", "detail"];
const before = renderStack(routes);
const after = renderStack(openLink(routes, "order/42"));
const ops = reconcile(before, after);
printOps(ops);
countFates(ops);
```

```text
kept: /Stack
kept: /Stack/Screen[list]
kept: /Stack/Screen[list]/Text
kept: /Stack/Screen[detail]
kept: /Stack/Screen[detail]/Text
mounted: /Stack/Screen[order-42]
mounted: /Stack/Screen[order-42]/Text
kept: 5
updated: 0
mounted: 2
unmounted: 0
commands: 2
```

`/Stack/Screen[order-42]` is **mounted**, and with it the `Text` under it is mounted. The other screens in the stack and the texts inside them are **kept**. If the user goes back, they find the place where they left off; the link did not reset the stack, it put a screen on top of it.

## When the App Is Closed

If the notification is tapped while the app is closed, there is no tree in memory. The platform starts the app and the first address is the link. The initial route is `list`, the link target is `order-42`; the stack is built with both.

```javascript
// main.mjs
import { reconcile, printOps, countFates } from "./fate.mjs";
import { renderStack } from "./stack.mjs";
import { openLink } from "./link.mjs";

const start = openLink(["list"], "order/42");
const ops = reconcile(null, renderStack(start));
printOps(ops);
countFates(ops);
```

```text
mounted: /Stack
mounted: /Stack/Screen[list]
mounted: /Stack/Screen[list]/Text
mounted: /Stack/Screen[order-42]
mounted: /Stack/Screen[order-42]/Text
kept: 0
updated: 0
mounted: 5
unmounted: 0
commands: 5
```

Because there is no previous tree, the whole tree is mounted: the stack, two screens, and two texts. There is no kept node, because there is nothing to keep. The same link does two different jobs in the tree depending on whether the app is open or closed: when open it mounts a single screen, when closed it mounts everything. If these two measurements are not kept apart, the cost of a link to the tree is misunderstood.

## Reading the Platform Fragments Line by Line

The `NotificationTap` function catches the moment the user taps the notification. The function reads the address from a field inside the notification and writes it to a variable; it draws nothing on the screen and adds no route to the stack. The last line tells the platform that the work is done. The job of this function is only to leave the address: converting the address to a route and writing it to the stack is the app's next step.

This division is important. The platform code knows that a link has arrived but does not know which screen will open; that knowledge is in the route table. The route table, in turn, does not know where the link came from. For this reason the two parts can change independently: when the form of the notification changes, the platform code is updated; when the screen names change, the route table is updated.

The `LinkListener` fragment shows that the address reaches the JavaScript side in two ways. The first call asks for the first address if the app was started with a link; if there is no address, nothing happens. The second call listens for addresses that arrive while the app is running. The cleanup function (`return () => sub.remove();`) removes the listener, so that when the screen's lifetime ends, the listening ends too. Both entries are tied to the same function, `onLink`. The model reflects this commonality with `openLink`: the open and closed paths use the same conversion, and the only difference is whether there is a stack in hand beforehand.

## Going Back

The user closes the screen opened by the link. The last route leaves the stack, and the screen the link added goes from the tree. We measure this too, because a link adds a screen to the stack, but the lifetime of this screen does not end with the link.

```javascript
// main.mjs
import { reconcile, printOps, countFates } from "./fate.mjs";
import { renderStack } from "./stack.mjs";

const ops = reconcile(
  renderStack(["list", "detail", "order-42"]),
  renderStack(["list", "detail"]),
);
printOps(ops.filter((op) => op.fate !== "kept"));
countFates(ops);
```

```text
unmounted: /Stack/Screen[order-42]
unmounted: /Stack/Screen[order-42]/Text
kept: 5
updated: 0
mounted: 0
unmounted: 2
commands: 2
```

The screen the link mounted and the text inside it are unmounted; the remaining five nodes are kept. Unmounting, the reverse of mounting, produces the same number of commands as mounting. The cost of a link to the tree is paid once for as long as the screen lives and once more when the screen closes: one mount, one unmount.

## A Second Link

The user may have gone to an order and, without going back, tapped a second notification. The stack has `list` and `order-42`, and the new link carries the address `order/43`.

```javascript
// main.mjs
import { reconcile, printOps, countFates } from "./fate.mjs";
import { renderStack } from "./stack.mjs";
import { openLink } from "./link.mjs";

const routes = ["list", "order-42"];
const ops = reconcile(renderStack(routes), renderStack(openLink(routes, "order/43")));
printOps(ops.filter((op) => op.fate !== "kept"));
countFates(ops);
```

```text
mounted: /Stack/Screen[order-43]
mounted: /Stack/Screen[order-43]/Text
kept: 5
updated: 0
mounted: 2
unmounted: 0
commands: 2
```

The second link did not remove the first from its place; it put a new screen on top of it. `order-42` is kept, `order-43` is mounted. The model made this choice with `AL1`: if the address is recognized and the target is not at the top, a route is added. A real app could in this case choose to replace the old order; the measurement would then give a different fate, and this lesson does not measure that choice. What is measured is the behavior the model built.

## What the Route Table Does

The route table is a small set of rules that bind addresses to route names. In our model this rule is a single line: an address in the form `order/<id>` becomes the route `order-<id>`. In a real app the table is longer, and each line defines a screen and sometimes the parameters of that screen. The table has two limits. For an address it does not recognize it produces nothing, which this lesson will show with the `unrecognized` measurement below. For an address it does recognize it produces only the route name; the content of the screen, that is, the order itself, comes from the network layer after the screen opens.

This second limit matters for the tree. The link mounts the screen, but the details of the order are not there yet. The screen first opens in the loading state, and then is updated when the data arrives. The cost of a link to the tree is therefore in two stages: the mounting of the screen and the arrival of the content. In this lesson only the first stage was measured; the second stage is the subject of the Networking and Data lesson.

## Launch Order

When the app is started with a link, there are two paths. In the first, the app asks for the first address and builds the tree directly with the stack the link points to; our measurement showed this path, and five nodes out of five were mounted. In the second, the app first builds the initial screen, learns the address later, and handles the link like an event that arrives in a running app. On this path, the three nodes of the initial stack (`Stack`, the `list` screen, and its text) are mounted first, and the two nodes of the link are added later; the total is again five mounted nodes, but the user sees only the initial screen for a brief moment.

The two paths are equal in terms of the number of commands. The difference between them is a difference not of the tree but of the intermediate state the user sees. The rule does not see the intermediate state; it sees only the two trees being compared. For this reason comparing these two paths is a design evaluation outside the tree fate.

## The Bounding Measurement

A link reaching the app does not mean the tree changes. The route table may not recognize the address, or the notification may arrive twice. In both cases the link has been processed but the tree does not change.

```javascript
// main.mjs
import { reconcile, countFates } from "./fate.mjs";
import { renderStack } from "./stack.mjs";
import { openLink } from "./link.mjs";

function measure(label, routes, link) {
  console.log(label);
  countFates(reconcile(renderStack(routes), renderStack(openLink(routes, link))));
}

measure("unrecognized:", ["list", "detail"], "campaign/7");
measure("duplicate:", ["list", "order-42"], "order/42");
```

```text
unrecognized:
kept: 5
updated: 0
mounted: 0
unmounted: 0
commands: 0
duplicate:
kept: 5
updated: 0
mounted: 0
unmounted: 0
commands: 0
```

In both measurements the commands are 0: all nodes are kept. The fate does not measure whether the link was processed; it measures what became of the tree. A notification may have been tapped, the app may have received the link, and the route table may have made a decision; no trace may remain in the tree.

## Common Misconceptions

- "A link arrived, so a new screen opened." A screen opens if a route is added to the route list. An unrecognized or a duplicate link does not change the list.
- "A notification always starts the app from scratch." If the app is open, the link arrives as an event and the stack is kept; a build from scratch happens only when the app is closed.
- "The notification itself is visible in the tree." The notification stays in the operating system's notification area; only the link that arrives when it is tapped enters the tree.
- "If the same notification arrives twice, two screens open." If the target route is already at the top, the model does not change the list. The duplicate link measurement gave 0 commands.
- "An unrecognized link gives an error." For an unrecognized address the model returns the list as it is and the tree does not change. If something is to be told to the user, this must be written to the tree as a separate node; then the measurement would show one mounted node instead of 0 commands.
- "A link always grows the stack." A link may add a route to the stack, may add nothing, or an app may choose to build the stack from scratch. This lesson measured only the adding behavior.
- "The notification permission shows the link in the tree." The permission determines whether the notification will be shown. Unless the notification that is shown is tapped, no link reaches the app and the tree does not change.
- "The same link always gives the same tree." The result depends on the stack at the moment the link arrives. The same `order/42` address produced five mounted nodes when the app was closed and there was no previous tree, and two mounted nodes in a stack with the `list` and `detail` screens. It is not possible to state the cost of a link by looking at the address alone.

## Summary

- This lesson's fate: mounted. When the app is open, the link mounts the `/Stack/Screen[order-42]` screen, and the screens below it are kept.
- When the app is closed there is no previous tree; the link makes the whole tree mounted.
- The same link mounts a single screen when the app is open and the whole tree when it is closed; the two cases are measured separately.
- An unrecognized and a duplicate link produce no commands in the tree; a link having been processed does not mean the tree changed.
- The platform code that carries the link as an address and the route table that converts it to a route are two separate jobs; the result of the second determines the fate.

## Next Step

Up to this lesson the tree changed in discrete steps: a tap, a response, a link. While a finger is dragged across the screen, the change happens in every frame. The last lesson, `Gestures and Animations`, measures how the command separates when the same movement is written to the tree in two ways.
