4 min read
Deferred deep linking in React Native and Expo with @deeplinknow/react-native
A walkthrough of installing @deeplinknow/react-native 1.2.0 in an Expo development build or a bare React Native app and reading the deferred link on first launch.
Rewritten in October 2026. The February 2025 version used an old package name and an API that has since changed, so I've replaced it.
The old version of this post said you could use the SDK with "no rebuilds". That was wrong. @deeplinknow/react-native 1.2.0 is a thin wrapper over the native iOS and Android SDKs, so you need a development build. The JavaScript side passes your calls to the Swift and Kotlin code and hands back what they return, which means a React Native app gets the same answer a native app would. The exact signals (the iOS clipboard and the Android Play Install Referrer) can only be read from native code. In Expo Go there's no native module, so the SDK falls back to calling the API from JavaScript, which can only do the fingerprint step, and it logs a warning saying so.
addDeepLinkListener, createDeepLink and the attribution field from the old post are gone as well. There's nothing to create, because any path on your link domain is already a link (your app or backend just builds https://myapp.deeplinknow.com/product/123?ref=share as a string), and DeepLinkNow returns no attribution data for the old field to hold.
You need React Native 0.81 or later (old or new architecture) or Expo SDK 54 or later, iOS 13, and Android API 24.
Install
In Expo:
npx expo install @deeplinknow/react-native
Add the config plugin to app.json with your link domains. For each one it writes an applinks: entry into the iOS Associated Domains and an auto-verified App Links intent filter on Android:
{
"expo": {
"plugins": [
["@deeplinknow/react-native", { "domains": ["myapp.deeplinknow.com", "go.myapp.com"] }]
]
}
}
Then npx expo prebuild (or let EAS Build do it) and npx expo run:ios or npx expo run:android.
In bare React Native it's the usual:
npm install @deeplinknow/react-native
cd ios && pod install
Autolinking picks the TurboModule on the new architecture and the bridge module on the old one. You add the Associated Domains entitlement (applinks:myapp.deeplinknow.com) and an autoVerify https intent filter yourself, and the iOS and Android docs walk through both. Either way, fill in your bundle and package details in the dashboard, since DLN builds apple-app-site-association and assetlinks.json from them on each request and there's no file for you to host.
First launch
Use the public key (dln_pub_…) from the dashboard. The SDK endpoints reject private keys, and you don't want a private key in an app bundle anyway.
import DeepLinkNow from "@deeplinknow/react-native";
export async function routeDeferredLink(
open: (path: string, params: Record<string, string | true | string[]>) => void
) {
await DeepLinkNow.initialize("dln_pub_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", {
enableLogs: __DEV__,
});
const link = await DeepLinkNow.checkDeferredDeepLink();
if (link) {
open(link.path, link.queryParams);
}
}
Neither call throws. initialize resolves null if it fails, and checkDeferredDeepLink resolves null for "no link" and for failures like being offline, so the app just carries on to its normal start screen. If you care which it was, DeepLinkNow.lastError holds a DLNError with a code such as network or notInitialized.
You get back path, queryParams, fullPath (path plus query string), url, source ("clipboard", "referrer" or "fingerprint"), confidenceScore and matchQuality. A fingerprint result is a best guess with a score attached, and if a wrong guess would cost you something you can set a floor:
const link = await DeepLinkNow.checkDeferredDeepLink({ minConfidence: 75 });
It returns a link at most once per install. After a definite answer, later calls resolve null without touching the network or the clipboard. Errors don't count, so an offline first launch gets another go next time. While you're testing, await DeepLinkNow.resetDeferredDeepLink() clears that state so you can run the flow again without reinstalling, and checkDeferredDeepLink({ force: true }) ignores the "already checked" flag.
On iOS the first call reads the pasteboard, which brings up the iOS 16+ "Allow Paste" prompt. The native SDK first checks, without prompting, whether the pasteboard probably holds a URL, and only reads it if so. Pass useClipboard: false if you'd rather it never read it, and you'll get fingerprint results only on iOS.
When the app is already installed
iOS and Android open the app straight from the link through Universal Links and App Links, and you parse the URL with parseDeepLink:
import { Linking } from "react-native";
import DeepLinkNow from "@deeplinknow/react-native";
// openScreen is your own navigation function.
function handleUrl(url: string | null) {
if (!url) return;
const link = DeepLinkNow.parseDeepLink(url);
if (link) {
openScreen(link.path, link.queryParams);
}
}
Linking.getInitialURL().then(handleUrl);
const subscription = Linking.addEventListener("url", ({ url }) => handleUrl(url));
Call subscription.remove() when you no longer need the listener. If you're on Expo Router, the Expo Router post covers the routing side, and the full API is in the React Native docs. parseDeepLink is synchronous and returns null for anything that isn't on your link domains, which makes it safe to hand it every URL the app receives.