All articles
Mobile DevelopmentOctober 7, 2026 6 min read

Deep Linking in React Native 2026: Universal Links, App Links, and the Edge Cases That Burn Teams

Universal Links on iOS and App Links on Android look simple in the docs. In production they fail silently, break after reinstall, and get hijacked by the share sheet. Here's what actually works in 2026.

Deep Linking in React Native 2026: Universal Links, App Links, and the Edge Cases That Burn Teams

Deep linking is one of those features that demos in five minutes and then eats a sprint six months later. Universal Links on iOS and App Links on Android look declarative and tidy in the documentation. In production they fail silently after an OS update, break on reinstall, get swallowed by in-app browsers, and route users to the wrong screen when a push notification lands at launch.

This is a field guide to the parts that actually hurt — the ones we hit shipping React Native and Expo apps across e-commerce, fintech, and marketplace clients.

The three things people conflate

Before anything else, separate the concepts. Teams routinely mix them up in Jira tickets and then wonder why the fix doesn't land.

  • Custom scheme links (myapp://product/123) — only work if the app is installed and the originating context honours the scheme. Great for OAuth callbacks, useless for marketing emails.
  • Universal Links (iOS) / App Links (Android) — real https:// URLs that the OS claims on behalf of your app. If the app isn't installed, the browser opens the page. If it is, the app opens directly.
  • Deferred deep linking — the user taps a link, doesn't have the app, installs it from the store, opens it for the first time, and still lands on the right screen. The OS gives you nothing here. You need a third party or your own attribution flow.

Most bugs we investigate are a confusion between these three. A product manager says "the deep link is broken" and means the install-attribution flow, while the engineer is debugging the AASA file.

Universal Links on iOS: the quiet failures

Apple's model is straightforward on paper. You host an apple-app-site-association (AASA) file at https://yourdomain.com/.well-known/apple-app-site-association, declare the paths your app owns, and add the Associated Domains entitlement.

In 2026 the thing that still trips teams up is that iOS does not re-fetch the AASA file aggressively. On a fresh install it fetches once. If you ship a bad AASA and users install during that window, their device may cache the failure. Apple's CDN-backed swcd (shared web credentials daemon) helps, but it is not a guarantee.

What to check first when a Universal Link stops working

  1. The AASA file is served over HTTPS with a valid certificate, Content-Type: application/json, and no redirects. A single 301 breaks it.
  2. The file is valid JSON, under 128KB, and does not have a .json extension in the URL.
  3. The Associated Domains entitlement in your app.json or Xcode project matches the domain exactly — applinks:www.yourdomain.com is not the same as applinks:yourdomain.com.
  4. The user did not long-press the link and choose "Open in Safari". iOS remembers that choice per-domain, per-app, and there is no public API to reset it.

For Expo projects, the config lives in app.json:

{
  "expo": {
    "ios": {
      "bundleIdentifier": "com.yourco.app",
      "associatedDomains": [
        "applinks:yourdomain.com",
        "applinks:www.yourdomain.com"
      ]
    }
  }
}

If you change this, you need a new native build. EAS Update will not ship entitlement changes — that is a managed workflow misunderstanding we see constantly.

The share sheet problem

Universal Links opened from inside apps like Instagram, TikTok, or Gmail's in-app browser often do not trigger your app. Those apps use WKWebView or SFSafariViewController variants that intentionally bypass Universal Links to keep users inside their ecosystem. There is no fix. The pragmatic workaround is a lightweight landing page that detects the user agent and shows an "Open in app" button that uses your custom scheme as a fallback.

Android App Links: verification is the whole game

Android's equivalent is the Digital Asset Links file at https://yourdomain.com/.well-known/assetlinks.json. The critical difference from iOS: Android verifies the link ownership and will refuse to open your app automatically if verification fails. Users then get the dreaded disambiguation dialog, or worse, the link opens in Chrome.

Since Android 12, verification is strict. If your assetlinks.json lists the wrong SHA-256 fingerprint for your signing key, verification fails and your app is permanently excluded from handling that domain until the user manually enables it in settings.

Two fingerprints matter and both must be present:

  • Your upload key (for sideloaded debug builds and some internal testing tracks)
  • The Google Play App Signing key (what users actually get)

The second one lives in Play Console under Setup → App integrity. Forgetting it is the single most common cause of broken App Links on production builds that worked fine in internal testing.

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.yourco.app",
    "sha256_cert_fingerprints": [
      "AA:BB:CC:...",
      "11:22:33:..."
    ]
  }
}]

Verify with:

adb shell pm get-app-links com.yourco.app

If you see verified next to your domain, you're good. If you see legacy_failure or 1024, verification failed and you need to redeploy the file and reinstall the app.

Wiring it into React Navigation without the cold-start bug

React Navigation's linking config handles the routing, but there is a race condition we hit repeatedly: on cold start from a deep link, the initial route sometimes renders before the linking URL is parsed, causing a flash of the home screen before the deep link destination mounts.

The fix is to use getInitialURL explicitly and gate your navigator on it:

import * as Linking from 'expo-linking';
import { NavigationContainer } from '@react-navigation/native';

const linking = {
  prefixes: [
    'https://yourdomain.com',
    'https://www.yourdomain.com',
    'myapp://'
  ],
  config: {
    screens: {
      Product: 'product/:id',
      Order: 'orders/:orderId',
      NotFound: '*'
    }
  },
  async getInitialURL() {
    const url = await Linking.getInitialURL();
    if (url) return url;
    // Also check push notification payload here
    return null;
  }
};

export default function App() {
  return (
    <NavigationContainer linking={linking} fallback={<Splash />}>
      <RootNavigator />
    </NavigationContainer>
  );
}

The fallback prop is non-optional in practice. Without it, users see a blank screen for the hundreds of milliseconds it takes to resolve the URL, and that is exactly when they tap again and double-navigate.

Push notifications are deep links in a trench coat

If your push payload contains a URL, treat it as a deep link and route through the same linking config. Do not maintain a parallel "notification routing" module — we have inherited two codebases where that split caused divergent behaviour and support tickets that took weeks to reproduce.

Deferred deep linking: pick your poison

There is no first-party way to do deferred deep linking well. Your options:

  • Branch, Adjust, AppsFlyer — paid attribution SDKs. They work, they add 1–3 MB to your binary, and they come with privacy manifest obligations that keep growing on iOS.
  • A fingerprint-based self-hosted solution — unreliable since iOS 14 and increasingly flagged by App Review.
  • Just use the clipboard — copy a token when the user taps your web CTA, read it on first launch, prompt for permission. iOS shows a paste notification banner now, which users notice. Not elegant but it is honest.
  • Skip it — a lot of apps overestimate how much deferred deep linking actually moves conversion. For an app with existing brand recognition, a plain Universal Link that falls back to the App Store page is often enough.

Measure before you integrate a vendor. The marketing team may already have attribution via a different tool.

App Review landmines

Two rejections we've seen more than once:

  • Guideline 2.1 — Universal Links that resolve to a 404 page when the app isn't installed. Reviewer taps your test link, sees a broken page, rejects. Always host a real landing page at every deep-linkable URL.
  • Google Play policy — intent filters that are too broad. If you claim android:host="*" or forget to scope android:pathPrefix, Play's review will flag it as a potential link hijack. Scope tightly.

Where we'd start

If you're adding deep linking to an existing React Native app today, do it in this order: get your AASA and assetlinks files hosted and verified first (that is often a week of back-and-forth with whoever controls DNS and the marketing site), then wire React Navigation's linking config and test cold-start on real devices, then unify push notification routing through the same config, and only then evaluate whether deferred deep linking is worth a paid SDK.

If you want a second set of eyes on an implementation that is misbehaving, our team does this kind of work regularly — see our mobile services or browse related breakdowns on the blog.

#react-native#expo#deep-linking#ios#android#mobile

Want a team like ours?

72Technologies builds production software for the kind of teams who actually read this blog.

Start a project