All articles
Mobile DevelopmentJuly 19, 2026 6 min read

Deep Links in React Native 2026: Universal Links, App Links, and the Cold-Start Traps

Deep linking in React Native looks trivial until a marketing team ships a campaign and half the users land on the home screen. Here's what actually breaks in 2026 — and how we route around it.

Deep Links in React Native 2026: Universal Links, App Links, and the Cold-Start Traps

Deep linking is one of those features that demos beautifully in a Monday standup and then quietly rots for six months. A marketing team schedules a campaign, someone clicks the link from Instagram's in-app browser on a cold-booted iPhone, and the app opens on the home tab with no route applied. Nobody notices until the click-through numbers look wrong.

We've shipped and repaired enough deep link setups across React Native and Expo apps to know the failure modes aren't in the docs. This is what we actually watch for in 2026.

What deep linking means now

The term still covers three different things, and mixing them up is where most bugs start.

  • Custom scheme links like myapp://product/123. These only work if the app is already installed and the OS knows the scheme.
  • Universal Links on iOS and App Links on Android. HTTPS URLs that open your app when installed and fall back to the web when not.
  • Deferred deep links, where a user without the app installs it and still lands on the intended screen. These require a third-party attribution SDK (Branch, AppsFlyer, Adjust) or a custom install-referrer flow.

In 2026, Apple and Google are both stricter about verified domain associations. If your apple-app-site-association file or your assetlinks.json isn't served correctly, the OS silently downgrades to opening Safari or Chrome. There's no crash, no log — just the wrong behavior.

The domain association files still trip teams up

Apple's apple-app-site-association must be served from https://yourdomain.com/.well-known/apple-app-site-association with Content-Type: application/json and no redirects. Android's assetlinks.json lives at https://yourdomain.com/.well-known/assetlinks.json and must match your app's signing certificate SHA-256 — the release one, not your debug key.

Quick sanity check we run on every project:

# Apple
curl -I https://yourdomain.com/.well-known/apple-app-site-association
# Expect: 200, Content-Type: application/json, no 301

# Google — verifies against Play Store signing key
curl "https://digitalassetlinks.googleapis.com/v1/statements:list?\
  source.web.site=https://yourdomain.com&\
  relation=delegate_permission/common.handle_all_urls"

If either of these returns anything except a clean, valid response, deep links will work inconsistently — often on some devices and not others, which is the worst kind of bug to reproduce.

Expo Router vs React Navigation linking config

If you're on Expo Router (which most new RN projects should be in 2026), deep linking is mostly convention-driven. Your file structure defines the URL structure. But you still need to declare the schemes and prefixes.

// app.json (Expo)
{
  "expo": {
    "scheme": "myapp",
    "ios": {
      "associatedDomains": ["applinks:yourdomain.com"]
    },
    "android": {
      "intentFilters": [
        {
          "action": "VIEW",
          "autoVerify": true,
          "data": [{ "scheme": "https", "host": "yourdomain.com" }],
          "category": ["BROWSABLE", "DEFAULT"]
        }
      ]
    }
  }
}

With bare React Navigation, you're maintaining a linking config manually:

const linking = {
  prefixes: ['myapp://', 'https://yourdomain.com'],
  config: {
    screens: {
      Product: 'product/:id',
      Checkout: 'checkout',
      NotFound: '*',
    },
  },
};

The manual config is more verbose but easier to reason about when things break. Expo Router's implicit routing is faster to ship, but debugging a wrong-route bug means reading through your app/ directory structure and hoping you understand how nested layouts resolve.

Our rule of thumb

Greenfield app, small team: Expo Router. Existing React Navigation codebase or complex conditional routing (auth walls, feature flags gating screens): stay explicit. Migrating just to be current is rarely worth the regression risk.

The cold-start trap

Here is the bug that keeps coming back on every project.

When a user taps a link and the app is already in memory, everything works. When the app is fully killed and the OS boots it up, the deep link URL is passed in through a different lifecycle path — and if your JavaScript bundle isn't ready yet, the initial URL can be swallowed.

Symptoms:

  • Works fine in dev builds every single time.
  • Works fine in production when you test manually (because your app is warm).
  • Fails ~30% of the time in the wild, especially on low-end Android where the app takes 3+ seconds to boot.

The fix is to grab the initial URL correctly and defer routing until your navigation stack is mounted.

import * as Linking from 'expo-linking';
import { useEffect, useState } from 'react';

export function useInitialDeepLink() {
  const [initialUrl, setInitialUrl] = useState<string | null>(null);
  const [ready, setReady] = useState(false);

  useEffect(() => {
    (async () => {
      const url = await Linking.getInitialURL();
      setInitialUrl(url);
      setReady(true);
    })();

    const sub = Linking.addEventListener('url', ({ url }) => {
      setInitialUrl(url);
    });
    return () => sub.remove();
  }, []);

  return { initialUrl, ready };
}

The critical part: don't render your navigator until ready === true. Show a splash. Otherwise the router mounts, computes an initial route based on no URL, and then your deep link fires after — and depending on the navigator, it may or may not replace the current stack cleanly.

Push notifications and deep links overlap more than you think

Every notification payload should carry a deep link URL, not a screen name and params. Screen names change when you refactor. URLs are your public contract.

{
  "aps": { "alert": { "title": "Order shipped" } },
  "link": "https://yourdomain.com/orders/98213"
}

When the notification is tapped, resolve the URL through the same linking logic as any other deep link. This gives you one code path to test and one place to fix bugs. We've seen teams maintain two separate routing systems — one for URLs, one for notifications — and they always drift.

Native Swift/Kotlin: worth it?

If you're building fully native, both platforms have cleaner primitives. Swift's SceneDelegate gives you continue userActivity for Universal Links directly, and Kotlin's Intent.data on onNewIntent is straightforward. The mental model is smaller because there's no JS bridge lifecycle to worry about.

But in a React Native app, going native for deep links alone is almost never the answer. The Expo Linking module and React Navigation's linking layer are mature. The bugs you'll hit are configuration bugs (association files, intent filters, cold-start timing), not framework bugs. Rewriting in Swift/Kotlin would just move the same configuration bugs to a different language.

The one case where we've reached for native: apps with a large existing native codebase where RN is embedded as a screen. There, letting the native side handle the initial URL and pass it into RN via a prop is cleaner than trying to bridge back.

What we actually test before shipping

A short checklist we run through before any release that touches routing:

  1. Kill the app fully. Tap a Universal Link from Notes on iOS. Does it open the right screen?
  2. Same test from an email client (Gmail, Outlook), because in-app browsers sometimes intercept.
  3. Same test on Android from a text message and from Chrome.
  4. Cold-start with a push notification carrying a deep link.
  5. Open the app, background it for 30 minutes, then tap a deep link. (Some OS aggressive-kill behaviors only show up here.)
  6. Fresh install from TestFlight / Play Internal Testing, then immediately tap a link.
  7. Verify apple-app-site-association and assetlinks.json return 200 from an incognito context, with no CDN cache serving stale versions.

Skip step 7 and you'll ship a broken domain association at some point. We've done it.

Where we'd start

If you're auditing deep linking on an existing app: run the two curl checks first. Half the deep link tickets we get handed are actually domain association misconfigurations that nobody noticed because engineers test on already-installed apps.

If you're greenfielding: pick Expo Router, wire up one Universal Link end-to-end before adding a second, and build the cold-start splash-until-ready pattern from day one. It's much harder to retrofit than to include upfront.

And if you want a second pair of eyes on a routing setup that keeps misbehaving, our mobile team has debugged enough of these to know where to look first.

#React Native#Expo#Deep Linking#iOS#Android#Mobile Development

Want a team like ours?

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

Start a project