React Native Deep Links
Handle deep links in your React Native app across iOS and Android, covering both cold start and warm start with the WarpLink SDK.
Deep links arrive in two scenarios: cold start (app launched by a link) and warm start (app brought from background by a link). The onLink callback you pass to configure() handles both. You do not need to wire up listeners in your components.
Basic Setup
Register onLink once at startup (see React Native SDK). Cold start, warm start, and deferred installs all flow through it:
import { WarpLink } from '@warplink/react-native';
WarpLink.configure({
apiKey: 'wl_live_yoursdkkeyhere000000000000000000',
onLink: ({ deepLink, error }) => {
if (error || !deepLink) return;
// Cold start, warm start, and deferred installs all arrive here.
navigateTo(deepLink.deepLinkUrl ?? deepLink.destination);
},
});
With React Navigation
Route to a screen from onLink using a navigation ref you set once. A cold-start link can arrive before the navigator has mounted, so hold it until the container is ready:
import { createNavigationContainerRef } from '@react-navigation/native';
import { WarpLink } from '@warplink/react-native';
export const navigationRef = createNavigationContainerRef();
let pendingUrl: string | null = null;
function openProduct(url: string): void {
if (!navigationRef.isReady()) {
pendingUrl = url;
return;
}
navigationRef.navigate('Product', { url });
}
export function flushPendingLink(): void {
const url = pendingUrl;
pendingUrl = null;
if (url !== null) openProduct(url);
}
WarpLink.configure({
apiKey: 'wl_live_yoursdkkeyhere000000000000000000',
onLink: ({ deepLink, error }) => {
if (error || !deepLink) return;
openProduct(deepLink.deepLinkUrl ?? deepLink.destination);
},
});
Pass ref={navigationRef} and onReady={flushPendingLink} to your NavigationContainer.
Cold Start vs Warm Start
| Scenario | Handled by | When |
|---|---|---|
| Cold start | onLink (automatic) | App was not running: launched by tapping a link |
| Warm start | onLink (automatic) | App was in background: brought to foreground by a link |
Both are delivered to onLink automatically. To drive them yourself, set automaticDeepLinks: false and use getInitialDeepLink() (cold start) and onDeepLink(listener) (warm start).
How the Bridge Works
@warplink/react-native is a classic native-module bridge: NativeModules['WarpLinkModule'] exposes the promise-based methods (configure, handleDeepLink, checkDeferredDeepLink, and friends), and a single NativeEventEmitter carries one event, onWarpLinkDeepLink, for every warm-start arrival on both platforms. React Native's interop layer runs classic native modules under Fabric and TurboModules without changes on either side, so nothing here differs between the classic and New Architecture: the SDK is exercised on both on every release, and there is no flag to set.
The JavaScript layer adds one thing neither native SDK does on its own: a shared arrival queue that classifies and claims each incoming URL in strict order, so two links arriving milliseconds apart cannot both slip past the in-app dedupe window before either has been recorded. That queue is invisible in normal use, but it is why onLink fires the events in the order links actually arrived rather than the order their underlying native resolves happened to finish.
Platform Asymmetry: The Host Hook
The host hook differs between platforms.
Android needs no host code beyond launchMode. The native module registers its own ActivityEventListener when the package links, so cold-start and warm-start intents reach the SDK automatically once android:launchMode="singleTask" is set on your launch Activity in AndroidManifest.xml. There is no method to call from your own Android code.
iOS requires an explicit forwarding call to resolve links. React Native does not forward incoming Universal Links to native modules on its own. Your AppDelegate (and, if your app declares a UIApplicationSceneManifest, your SceneDelegate as well) must call WarpLinkModule.handleIncomingURL(url) directly:
// AppDelegate.swift
func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL {
WarpLinkModule.handleIncomingURL(url)
}
return RCTLinkingManager.application(
application, continue: userActivity, restorationHandler: restorationHandler)
}
handleIncomingURL does no domain filtering and returns nothing, so it is safe to call on every incoming URL, including ones that turn out to be OAuth callbacks or custom-scheme links unrelated to WarpLink; the SDK resolves what it recognizes and silently drops the rest. If your app declares a scene manifest (the default for a project generated from a recent Xcode template), iOS routes delivery to SceneDelegate instead and stops calling the AppDelegate hook at all, with no error and no crash, so the app looks healthy while every link quietly fails to resolve. Add the same call to scene(_:willConnectTo:options:), scene(_:continue:), and scene(_:openURLContexts:).
Interplay with React Native's Linking API
Most React Native apps already use Linking.addEventListener('url', ...) or React Navigation's linking config for their own routing. WarpLink does not replace this, and the two coexist:
- On iOS, forward incoming URLs to both
WarpLinkModule.handleIncomingURL(url)andRCTLinkingManagerfrom the same delegate method. Store each call's return value in a local variable before combining them; a||short-circuit means a handler placed after one that already returnedtruenever runs. - On Android, both mechanisms read from the same
Intent, and no code changes are required beyond the singleWarpLink.configure()call: the SDK's ownActivityEventListenerand React Native'sLinkingmodule do not compete for the intent. - A URL that is not a WarpLink link (a custom OAuth scheme, a link on a domain your org does not own) still reaches an explicit
onDeepLink()subscriber, delivered as an{ error }event with codeE_INVALID_URL, but is silently dropped by the automaticonLinksink. Your ownLinkinglistener sees it regardless, since it is never routed through WarpLink at all.
WarpLink's own getInitialDeepLink() returns the cold-start URL on scene-based iOS apps, because handleIncomingURL buffers it natively before JavaScript starts.
Expo Compatibility
The SDK requires native modules and linking, so it works with an Expo development build (bare workflow with native code, or a managed project built with npx expo prebuild / EAS Build) but not with Expo Go. Use a development build; Expo Go does not include custom native modules. expo prebuild regenerates a stock AppDelegate and AndroidManifest.xml, so add the WarpLink hook to each as shown below. Two options keep the integration working across prebuilds:
- Commit the generated
ios/andandroid/directories to version control and hand-editAppDelegate/SceneDelegatewith the host hook shown above; Android needs only theintentFiltersentry inapp.json, because theMainActivitythat prebuild generates already declaresandroid:launchMode="singleTask". - Write a small Expo config plugin using
withAppDelegatethat inserts thehandleIncomingURLcall on everyprebuildrun, so a clean or CI-triggered prebuild never drops it silently.
Associated Domains and the App Links intent filter both have app.json equivalents (ios.associatedDomains and android.intentFilters), so those two pieces need no manual native editing even without a config plugin; only the iOS host hook does.
Manual Setup (Advanced)
With automaticDeepLinks: false, wire the listeners yourself in your root component:
import { useEffect } from 'react';
import { WarpLink } from '@warplink/react-native';
function App() {
useEffect(() => {
const unsubscribe = WarpLink.onDeepLink((event) => {
if (event.deepLink) navigateTo(event.deepLink.destination);
else if (event.error) console.error('Deep link error:', event.error.message);
});
WarpLink.getInitialDeepLink().then((link) => {
if (link) navigateTo(link.destination);
});
return unsubscribe;
}, []);
return <>{/* Your app */}</>;
}
Working with Deep Link Data
const link = await WarpLink.handleDeepLink('https://aplnk.to/abc123');
if (link) {
console.log('Link ID:', link.linkId);
console.log('Destination:', link.destination);
// Platform-specific deep link URL
if (link.deepLinkUrl) {
navigateToPath(link.deepLinkUrl);
}
// Custom parameters
const productId = link.customParams['product_id'] as string | undefined;
if (productId) {
showProduct(productId);
}
}
Error Handling
import { WarpLinkError, ErrorCodes } from '@warplink/react-native';
try {
const link = await WarpLink.handleDeepLink(url);
} catch (error) {
if (error instanceof WarpLinkError) {
switch (error.code) {
case ErrorCodes.E_NOT_CONFIGURED:
console.error('Call configure() first');
break;
case ErrorCodes.E_INVALID_URL:
console.error('Not a WarpLink URL');
break;
case ErrorCodes.E_LINK_NOT_FOUND:
showLinkExpired();
break;
case ErrorCodes.E_NETWORK_ERROR:
showOfflineMessage();
break;
default:
console.error(`[${error.code}] ${error.message}`);
}
}
}
Listener Events
The onDeepLink listener receives a discriminated union. Exactly one of deepLink or error is present:
WarpLink.onDeepLink((event) => {
if (event.deepLink) {
// Success: resolved deep link
console.log(event.deepLink.destination);
} else if (event.error) {
// Error: resolution failed
console.error(event.error.code, event.error.message);
}
});
Multiple listeners can be registered simultaneously. Each receives every event. The native event subscription is cleaned up when the last listener is removed.
Troubleshooting
A tapped link opens the browser instead of the app. The association files are the usual cause. Paste your link domain into our deep link checker to read both files. To write the native setup for a domain of your own, use our React Native deep linking config generator.
iOS: the app opens but onLink never fires. This is the host hook step described above. Check whether your AppDelegate calls WarpLinkModule.handleIncomingURL(url) at all, then check whether your app declares a UIApplicationSceneManifest in Info.plist; if it does, the AppDelegate methods you added the call to have stopped running and the call needs to move to SceneDelegate instead.
"The package '@warplink/react-native' doesn't seem to be linked." Three causes, in order of likelihood: you're running in Expo Go (switch to a development build), you installed the package but never rebuilt the native app (a Fast Refresh reload is not enough; run npx react-native run-ios / run-android again), or pod install was never run after adding the dependency on iOS.
Android: cold-start links work, warm-start links do nothing. android:launchMode="singleTask" is missing from the main Activity in android/app/src/main/AndroidManifest.xml. Without it, Android starts a fresh Activity for the incoming intent instead of routing it to the running one through onNewIntent, and the native module never sees the URL to emit an event for.
pod install fails on Apple Silicon, or with SPM-related errors. The WarpLink iOS SDK is pulled in via Swift Package Manager through spm_dependency in the RN podspec, which requires use_frameworks! :linkage => :dynamic in your Podfile. Confirm that line is present in your Podfile.
Deferred deep link check always returns null, but direct links resolve fine. Check which credential was passed to configure(). Both an API key and an SDK key match the wl_live_ + 32-character format, so a swapped key does not fail the format check and deep links keep working, but the deferred attribution request is rejected every time. Create a dedicated SDK key under Keys > SDK keys in the dashboard.
FAQ
Does the New Architecture (Fabric/TurboModules) need any special setup? No. The SDK is written against the classic NativeModules/NativeEventEmitter API, and React Native's interop layer runs it unmodified under Fabric. Nothing in your integration code changes between the classic and New Architecture.
Why does a rapid double-tap on the same link only fire onLink once? The bridge applies a short dedupe window (1.5 seconds) to the automatic path, mirroring similar windows in the native iOS and Android SDKs, so a genuine duplicate tap on one link inside that window is treated as one navigation rather than two. An explicit onDeepLink() listener still receives every event if you need the raw stream.
Can I use onDeepLink() and the automatic onLink callback at the same time? Yes. They are independent subscribers to the same underlying event stream. A URL the automatic path treats as foreign and drops is still delivered to an explicit onDeepLink() listener as an { error } event, since an explicit subscriber asked for every event rather than only the ones WarpLink resolves.
How are taps on different links handled? If a second, different link is tapped while the first is still resolving, the bridge treats the newer tap as superseding the older one and lets the newer resolve proceed; the two are different navigations, not a duplicate of the same one, so both are expected to complete.
React Native SDK
Integrate the WarpLink React Native SDK for Universal Links on iOS, App Links on Android, deferred deep links, and install attribution.
React Native Deferred Deep Links
Preserve a link's destination through App Store and Play Store installs, then route the user on first launch, in React Native.