DEV Community

Cover image for Deferred Deep Links in React Native: Complete Integration Guide
LinkTrail
LinkTrail

Posted on

Deferred Deep Links in React Native: Complete Integration Guide

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 Linking API 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
Enter fullscreen mode Exit fullscreen mode

2. Native configuration

iOS — Associated Domains

In Xcode, enable the Associated Domains capability and add your link domain:

applinks:links.yourapp.com
Enter fullscreen mode Exit fullscreen mode

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": [{ "/": "/*" }]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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"]
  }
}]
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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');
  }
}
Enter fullscreen mode Exit fullscreen mode

5. Test both paths before you trust either

This is the step people skip, and it's the one that actually matters:

  1. Direct case — install the app, background it, tap a link. Confirm addLinkListener fires with the right path.
  2. Deferred case — uninstall completely, tap the same link, get sent to the store, install, open cold. Confirm getFirstOpenLink resolves with the same path — not just that the app opens.
  3. Run your static files through a validator, not just a browser tab — you want to know if assetlinks.json is 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)

Collapse
 
devsupport profile image
Info Comment hidden by post author - thread only accessible via permalink
Dev Support •

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