In React Native, how do Linking.getInitialURL() and Linking.addEventListener('url', …) differ, and why does an app need both?
answer
- killed versus already running
- one is a Promise, one is an event
- string or null on cold start
- events are not replayed to late listeners
- subscription.remove() on cleanup
basics
~20 sLinking.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 linesimport { 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
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.
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.
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.
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