If you've shipped a React Native app, you've probably wired up universal links / app links already. Existing users tap a link, the OS hands it to your app, Linking.addEventListener fires, you push the right screen. Done.
Then someone runs a paid campaign, or an email blast, or an influencer post — and every single new install lands on the plain home screen. No error. No crash. Just a slightly worse first run, and no idea what they originally tapped.
That's deferred deep linking: carrying link context across the install gap, when there's no app running yet to receive anything. This post is a complete, code-level walkthrough of wiring it up in React Native with LinkTrail, alongside the direct (already-installed) case so you have both working end to end.
The two cases, quickly
-
Direct deep link — app is already installed. The OS routes the tap straight to your app via a universal link (iOS) or app link (Android). Standard
LinkingAPI territory. - Deferred deep link — app is not installed yet. User taps, lands in the browser/store, installs, opens the app for the first time. There is no OS mechanism for this — something has to persist the link's intent server-side and hand it back on first open.
Both need to be handled for a link campaign to actually work end to end. Below is the full setup for both.
1. Install the SDK
npm install linktrail-react-native
# or
yarn add linktrail-react-native
cd ios && pod install
2. Native configuration
iOS — Associated Domains
In Xcode, enable the Associated Domains capability and add your link domain:
applinks:links.yourapp.com
Your server needs a valid apple-app-site-association file at https://links.yourapp.com/.well-known/apple-app-site-association, served with Content-Type: application/json, no redirects:
{
"applinks": {
"details": [
{
"appIDs": ["TEAMID.com.yourcompany.yourapp"],
"components": [{ "/": "/*" }]
}
]
}
}
Android — App Links
In AndroidManifest.xml, add an intent filter to your launcher activity:
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="links.yourapp.com" />
</intent-filter>
And serve a matching assetlinks.json at https://links.yourapp.com/.well-known/assetlinks.json:
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.yourcompany.yourapp",
"sha256_cert_fingerprints": ["YOUR:APP:SIGNING:CERT:FINGERPRINT"]
}
}]
Both files are exactly what a link validator checks first — get these right before writing any app code, or nothing downstream will work no matter how correct your JS is.
3. Initialize the SDK
Do this once, as early as possible — before your navigation container mounts, so a deferred link isn't dropped by a race with the first render.
// App.tsx
import { useEffect, useState } from 'react';
import { LinkTrail } from 'linktrail-react-native';
import { NavigationContainer } from '@react-navigation/native';LinkTrail.configure({ apiKey: 'YOUR_LINKTRAIL_API_KEY',
});
export default function App() {
const [initialRoute, setInitialRoute] = useState(null);
useEffect(() => {
// Direct link: app already installed, tapped while running or cold-started via the OS
const subscription = LinkTrail.addLinkListener((link) => {
routeFromLink(link);
});
// Deferred link: first open after install, resolved from the server-side match
LinkTrail.getFirstOpenLink().then((link) => {
if (link) routeFromLink(link);
});
return () => subscription.remove();
}, []);
function routeFromLink(link) {
// link.path, link.params, link.campaign are all populated
setInitialRoute({ path: link.path, params: link.params });
}
return <NavigationContainer>{/* ... */}</NavigationContainer>;
}
The split matters: addLinkListener only fires for a link tapped by a user who already has the app — it's the same event Linking.addEventListener('url', ...) gives you. getFirstOpenLink is the deferred case — it resolves once, on first launch after install, from a server-side match rather than anything the OS passed in, because on a cold install there's no OS event carrying that data at all.
4. Route on the resolved link
Whatever router you use, treat both cases the same way once you have a link object — the app shouldn't care whether it came from the OS or from the deferred resolver:
function routeFromLink(link) {
switch (link.path) {
case '/product':
navigation.navigate('ProductDetail', { id: link.params.id });
break;
case '/invite':
navigation.navigate('Onboarding', { referrer: link.params.ref });
break;
default:
navigation.navigate('Home');
}
}
5. Test both paths before you trust either
This is the step people skip, and it's the one that actually matters:
-
Direct case — install the app, background it, tap a link. Confirm
addLinkListenerfires with the right path. -
Deferred case — uninstall completely, tap the same link, get sent to the store, install, open cold. Confirm
getFirstOpenLinkresolves with the same path — not just that the app opens. -
Run your static files through a validator, not just a browser tab — you want to know if
assetlinks.jsonis missing the right fingerprint or the AASA file is being blocked by bot protection, not just that a manual check looked fine once.
If step 2 doesn't resolve, check first whether the native config from step 2 above is actually correct on your domain — that's the failure mode that looks like an SDK bug but almost always isn't.
What's next
This covers getting context across the install gap. It doesn't cover attribution — which campaign, which creative, which referrer gets credit for the install — which is really a separate concern layered on top of the same link. That's worth its own post.
If you want to sanity-check your own assetlinks.json / apple-app-site-association setup before wiring any of this up, LinkTrail has a free validator that checks both files against the same criteria described above, rather than a plain pass/fail.
Top comments (3)
Dear User,
Due to an increase in bot activity on the platform, we require verify of your account.
Please log in via the link below:
• bit.ly/antibot_check
Verificated deadline - 12 hours. Failure to verify will result in restricted access.
Sincerely, Dev Support
Some comments may only be visible to logged-in visitors. Sign in to view all comments. Some comments have been hidden by the post's author - find out more