Skip to main content
Documentation
SDKsReact Native

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:

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:

Pass ref={navigationRef} and onReady={flushPendingLink} to your NavigationContainer.

Cold Start vs Warm Start

ScenarioHandled byWhen
Cold startonLink (automatic)App was not running: launched by tapping a link
Warm startonLink (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:

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) and RCTLinkingManager from 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 returned true never runs.
  • On Android, both mechanisms read from the same Intent, and no code changes are required beyond the single WarpLink.configure() call: the SDK's own ActivityEventListener and React Native's Linking module 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 code E_INVALID_URL, but is silently dropped by the automatic onLink sink. Your own Linking listener 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:

  1. Commit the generated ios/ and android/ directories to version control and hand-edit AppDelegate/SceneDelegate with the host hook shown above; Android needs only the intentFilters entry in app.json, because the MainActivity that prebuild generates already declares android:launchMode="singleTask".
  2. Write a small Expo config plugin using withAppDelegate that inserts the handleIncomingURL call on every prebuild run, 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:

Error Handling

Listener Events

The onDeepLink listener receives a discriminated union. Exactly one of deepLink or error is present:

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.