skip to content

Deep Link Handling

Opening the app from a URL takes a registered custom scheme or verified domain plus code that routes the link on cold and warm start. Interviewers probe links that silently open the browser.

part ofReact Nativeoverview, primer and where to startread it →
on this pageshow

explore

questions

14

In React Native, how do Linking.getInitialURL() and Linking.addEventListener('url', …) differ, and why does an app need both?

level: juniorimportance: must knowfreq 60%

answer

  1. killed versus already running
  2. one is a Promise, one is an event
  3. string or null on cold start
  4. events are not replayed to late listeners
  5. subscription.remove() on cleanup

basics

~20 s

Linking.getInitialURL() resolves to the URL that launched a killed app, or null; the 'url' event fires when a link arrives while the app is already running. Handling only one loses either cold-start or warm-start links.

solid answer

~40 s

`Linking.getInitialURL()` returns a Promise of the URL that **launched** the app from a killed state, or `null` if the launch was not from a link; later warm-start links do not change it, and a JavaScript reload reads the same launch URL again. `Linking.addEventListener('url', handler)` covers the other case: when a link arrives while the app is already running, in the foreground or background, the handler receives `{url}`. A cold start never fires the event, and a warm-start link never becomes the initial URL, so an app that handles only one path silently drops the other. The event is also not replayed, so the listener must be registered early, at the app root, and removed with the returned subscription's `remove()` on cleanup. React Navigation's `linking` option and Expo Router handle both paths for you.

code

tsx · 24 lines
tsx
import { useEffect, useState } from 'react';
import { Linking } from 'react-native';

export function useIncomingLink(): string | null {
  const [url, setUrl] = useState<string | null>(null);

  useEffect(() => {
    let active = true;
    const subscription = Linking.addEventListener('url', event => {
      setUrl(event.url);
    });
    Linking.getInitialURL().then(initial => {
      if (active && initial) {
        setUrl(current => current ?? initial);
      }
    });
    return () => {
      active = false;
      subscription.remove();
    };
  }, []);

  return url;
}

go deeper

for a junior

Recall that getInitialURL covers links that launch a killed app and the url event covers links that reach a running app, and that you need both.

for a middle

Explain what each path reads on iOS and Android, that later links do not update getInitialURL, and that the event is not replayed to late listeners.

for a senior

Show how you would find which path lost a link, handle reload and duplicate delivery of one-time tokens, and place the listener where it is always mounted.

for a principal

Decide whether link handling lives in the navigation library's linking config or in an app-level router so auth gating and analytics see every link consistently.

## Two moments a link can arrive A user taps `myapp://auth/magic?token=abc` (or an equivalent verified `https` link) in their mail app. What React Native must do depends on the state of the app at that moment: | App state when the link is tapped | What the OS does | How JavaScript sees the URL | |---|---|---| | **Killed** (not running) | launches the process and passes the URL as launch data | `Linking.getInitialURL()` resolves to it | | **Running** (foreground or background) | brings the app forward and delivers the URL to the running instance | `Linking` emits a `'url'` event with `{url}` | These are called **cold start** and **warm start** routing. The two paths are separate: a cold start does not emit the `'url'` event, and a link delivered to the running app does not change what `getInitialURL()` reports. ## getInitialURL() - Signature: `getInitialURL(): Promise<string | null>`. - It resolves to the URL that launched the app, or `null` when the app was opened from the home screen, a notification without a link, or anything else. - On iOS, `RCTLinkingManager` reads it from the launch options the app delegate received, including the web-browsing activity of a Universal Link. On Android, the intent module reads the launching activity's `Intent` and returns its data when the action is `VIEW`. - It is **not** "the latest URL". Later warm-start links do not update it, and a JavaScript reload in development reads the same launch URL again. Code that consumes a one-time token must remember that it already handled it. ## addEventListener('url', handler) - Signature: `Linking.addEventListener('url', ({url}) => ...)`, returning an `EventSubscription`. - It fires for links delivered to an app that is already running. - Cleanup is `subscription.remove()`. The old `Linking.removeEventListener` no longer exists in current React Native. - The event is **not buffered**. If the app registers the listener only inside a screen that mounts later, a link that arrives earlier is simply missed. ## Putting them together A robust handler, usually at the root of the app: 1. Subscribe to `'url'` as early as possible. 2. Call `getInitialURL()` once on startup and handle a non-null result. 3. If both deliver a URL, prefer the newer one, which is the event. 4. Remove the subscription when the root unmounts. Most apps never write this by hand: React Navigation's `linking` option on `NavigationContainer` calls `getInitialURL()` and subscribes to `'url'` itself, and Expo Router handles both paths for every route automatically. Expo also ships `expo-linking`, whose `useLinkingURL()` hook returns the launch URL and then any later one. Knowing the two underlying paths still matters when a link goes missing, because the fix is usually in one path only. ## How to test both paths Because the two paths are separate, test each one deliberately: 1. **Cold**: force-stop the app, then open the link from outside it, for example with `adb shell am start -a android.intent.action.VIEW -d "myapp://auth/magic?token=test"` on Android or `xcrun simctl openurl booted "myapp://auth/magic?token=test"` on an iOS simulator. Log the result of `getInitialURL()`. 2. **Warm, foreground**: with the app open, fire the same command and log inside the `'url'` handler. 3. **Warm, background**: send the app to the background first, then fire the link; it should also arrive as the `'url'` event. A link that works in only one of these three runs tells you exactly which path to fix. ## Common mistakes - Handling only the `'url'` event: links work while testing with the app open and fail after the user swipes the app away. - Handling only `getInitialURL()`: the magic link works when the app was killed and does nothing when it was already open in the background. - Registering the listener in a screen that is not mounted, such as a sign-in screen hidden behind a splash. - Treating the initial URL as fresh after a reload, which re-submits a token that was already used. ## Why the platform split exists The split mirrors the operating systems. On iOS a URL arrives either in the launch options or through the app delegate's open-URL and continue-user-activity callbacks. On Android it arrives either in the launching `Intent` or, for an activity using `launchMode="singleTask"`, through `onNewIntent`. React Native maps the first of each pair onto `getInitialURL()` and the second onto the `'url'` event.

  • Why can a magic-link token be submitted twice during development after a JavaScript reload?
    `Linking.getInitialURL()` returns the URL that launched the process, and a JavaScript reload does not relaunch the process, so the same launch URL comes back. If the handler exchanges the token every time it sees it, the second exchange fails as already used. Track the handled URL, or with `expo-linking` call `Linking.clearInitialURL()` after consuming it.
  • What replaced Linking.removeEventListener in current React Native?
    `Linking.addEventListener('url', handler)` returns an `EventSubscription`, and you call `subscription.remove()` in the effect cleanup. `removeEventListener` is gone, so code copied from old tutorials fails at runtime. The subscription pattern is the same one other React Native event emitters use.
  • Does the 'url' event fire when the app is in the background rather than the foreground?
    Yes. A running app that is backgrounded is brought to the foreground and the link is delivered to the existing JavaScript instance as a `'url'` event. Only a killed app goes through `getInitialURL()`. The same instance keeps its state, which is why warm-start routing has to push onto an existing navigation stack.

getInitialURL() is the note someone left on your desk before you arrived; the 'url' event is the doorbell while you are home. You must read the note once and keep listening for the bell, and a bell rung before you were listening is not replayed.

saying these in an interview costs you the question

  • getInitialURL() returns the most recent link the app received
  • A cold start also fires the 'url' event, so the listener is enough
  • Linking.removeEventListener is the way to unsubscribe
  • The 'url' event is queued until a listener subscribes
  • A backgrounded app goes through getInitialURL() again
open as a page

In React Native, how does Linking.openURL open the mail app, the dialer or a browser from a password-reset screen, and when does it fail?

level: juniorimportance: must knowfreq 60%

basics

~20 s

React Native's Linking.openURL hands a URL to the operating system, which opens the app registered for its scheme: mailto: for mail, tel: for the dialer, https: for the browser. Its promise rejects when nothing can open the URL.

open as a page

In a bare React Native app, how do you register a custom URL scheme like resetapp:// on iOS and Android?

level: middleimportance: must knowfreq 50%

basics

~20 s

Register the scheme natively: on iOS add it under CFBundleURLTypes / CFBundleURLSchemes in Info.plist; on Android add an intent-filter with the VIEW action, DEFAULT and BROWSABLE categories and a data scheme to MainActivity. Then rebuild the app.

open as a page

In a React Native app, what must the website and the native projects configure so iOS Universal Links and Android App Links verify?

level: middleimportance: must knowfreq 55%

basics

~10 s

The domain hosts /.well-known/apple-app-site-association (Team ID plus bundle ID, paths) and /.well-known/assetlinks.json (package name, SHA-256 signing fingerprints) over HTTPS; the app declares applinks:domain in Associated Domains and an https intent filter with autoVerify.

open as a page

In an Expo project, what does the app config's scheme field do, and why does changing it require a new build rather than an update?

level: juniorimportance: should knowfreq 38%

basics

~20 s

Expo's scheme field declares the app's custom URL scheme; prebuild writes it into Info.plist's CFBundleURLTypes and an Android VIEW intent filter. That is native configuration, so it needs a new build — an over-the-air update cannot add it.

open as a page

In a React Native app, what is a Universal Link or Android App Link, and what happens when someone taps one without the app installed?

level: juniorimportance: should knowfreq 50%

basics

~20 s

A Universal Link (iOS) or App Link (Android) is a plain https URL the OS opens straight in your app because the domain has verified the app. Without the app installed, the same URL opens the website in the browser.

open as a page

In a bare React Native app, what native wiring lets incoming links reach the Linking module on iOS and on Android?

level: middleimportance: should knowfreq 40%

basics

~10 s

On iOS the AppDelegate forwards the open-URL and continue-user-activity callbacks to RCTLinkingManager and passes launchOptions to startReactNative; on Android, MainActivity uses launchMode singleTask so running-app links arrive through onNewIntent.

open as a page

In React Native, why can Linking.canOpenURL for another app's custom scheme return false or reject even though that app is installed?

level: middleimportance: should knowfreq 44%

basics

~20 s

Both platforms restrict which apps yours may query. On iOS, a custom scheme missing from LSApplicationQueriesSchemes makes React Native's canOpenURL reject; on Android 11+, a scheme not declared in the manifest's queries element makes it resolve false.

open as a page

In a React Native app, why is a custom scheme link like resetapp://reset?token=… a poor carrier for a password-reset token, and what is scheme hijacking?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Any app can register the same custom scheme, and neither platform guarantees yours receives it, so a malicious app can intercept resetapp:// links and steal the token. Sensitive links belong on verified https domains, with short-lived tokens and validation.

open as a page

Your React Native app's recipe App Links open the app from a locally signed release build but open the browser once installed from Google Play; what is the likely cause?

level: seniorimportance: should knowfreq 35%

basics

~20 s

With Play App Signing, Google re-signs the app with the app signing key, so an assetlinks.json listing only the upload or local key's SHA-256 fingerprint no longer matches; add the app signing key's fingerprint from Play Console.

open as a page

Shared recipe links like https://recipes.example.com/r/42 open Safari instead of your React Native iOS app, with no error anywhere; how do you diagnose it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Treat it as failed verification: fetch the AASA and confirm HTTPS, 200 and no redirect; check its Team ID, bundle ID and paths; confirm the signed build carries applinks:host; then reinstall, because iOS caches the file.

open as a page