Mastering Deep Linking React Native: A 2026 Guide

Master deep linking react native and Expo. This end-to-end guide covers iOS Universal Links, Android App Links, React Navigation, auth, and testing.

Profile photo of ParthParth
16th Jul 2026
Featured image for Mastering Deep Linking React Native: A 2026 Guide

You shipped the campaign link. The user taps it from email on an iPhone or Android device. Your React Native app opens, but it lands on the home screen instead of the product, invite, reset-password form, or checkout step they expected.

That's the moment when deep linking stops being a “nice feature” and becomes infrastructure.

Most deep linking React Native tutorials solve one slice of the problem. They show a custom URL scheme, or a React Navigation snippet, or a server-side file for domain verification. Production apps need all of it wired together. The domain has to verify correctly, the app has to parse links in every runtime state, and auth has to resume the original intent instead of discarding it. If any one piece is wrong, the flow breaks.

Teams also forget the link before the app. If you're distributing app URLs through campaigns, SMS, or social posts, link formatting and shortening matter too. A clean redirect strategy paired with a solid guide to creating bit links helps when you need shareable URLs that still preserve deep-link intent.

Table of Contents

Why Deep Linking Is Crucial for Modern Apps

A broken deep link usually looks harmless in development. The app opens. No crash. No red screen. But the user still doesn't reach the screen they wanted, and that's a product bug, not just a routing bug.

In production React Native apps, Universal Links on iOS and App Links on Android are a foundational requirement, and campaign performance is often judged by whether the links move users into the right in-app destination. One benchmark cited for successful campaigns is a minimum click-through rate of 2 to 3 percent, with conversion around 10 to 15 percent, and a 30-day retention target above 20 percent for deep-linked entry points when teams want to validate whether those links lead users to valuable content instead of a fast exit (MoldStud on advanced deep linking techniques).

That's why deep linking React Native work isn't just navigation plumbing. It sits directly between acquisition and product usage.

Practical rule: If a marketing link opens the app but misses the intended screen, count it as a failed deep link.

The common failure pattern is simple:

  • The campaign is specific: an email promises one item, one discount, or one account action.
  • The app is generic: it launches to the default stack entry.
  • The user loses context: they don't know what to tap next.
  • The product team misreads the outcome: analytics show an “app open,” but the user never reached the destination.

Deep links also reduce friction outside marketing. Password reset emails, magic links, invite flows, support tickets, referral codes, and notification handoffs all depend on reliable route targeting. For startups, that matters even more because the app often has fewer screens and fewer chances to recover from a bad first landing.

The shortest way to think about it is this. Your website, email system, ad platform, and push notifications all speak in URLs. Your app speaks in navigation state. Deep linking is the translator.

Foundational Setup for Expo and Bare React Native

A laptop on a wooden desk displaying React Native code for deep linking setup in VS Code.A laptop on a wooden desk displaying React Native code for deep linking setup in VS Code.

Choose your primary link type early

Don't start with custom schemes and assume you'll “upgrade later.” For production, use Universal Links and App Links as the primary path, and keep a custom scheme as a fallback. That recommendation also aligns with the guidance summarized in this React Native deep linking best-practices discussion.

Custom schemes are still useful. They're easy to test and helpful as a backup. But they don't replace verified HTTPS links.

One more production rule matters before you write any screen mapping. Deep linking must work across foreground, background, and killed state, and authenticated routes add a second layer where the user may or may not already be logged in. The killed-state path is where many apps fail because they only handle link events after the app is running, not the initial launch URL.

Expo setup that won't fight React Navigation

If you're using Expo, keep the initial config boring and explicit.

In app.json:

{
  "expo": {
    "scheme": "myapp",
    "ios": {
      "bundleIdentifier": "com.example.myapp",
      "associatedDomains": ["applinks:example.com"]
    },
    "android": {
      "package": "com.example.myapp",
      "intentFilters": [
        {
          "action": "VIEW",
          "autoVerify": true,
          "data": [
            {
              "scheme": "https",
              "host": "example.com",
              "pathPrefix": "/"
            }
          ],
          "category": ["BROWSABLE", "DEFAULT"]
        }
      ]
    }
  }
}

In your app entry:

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

const prefix = Linking.createURL('/');

const linking = {
  prefixes: [prefix, 'https://example.com', 'myapp://'],
  config: {
    screens: {
      Home: '',
      Profile: 'profile',
      Product: 'products/:id',
      ResetPassword: 'reset-password',
    },
  },
};

export default function App() {
  return (
    <NavigationContainer linking={linking}>
      {/* navigators */}
    </NavigationContainer>
  );
}

For Expo projects, expo-linking gives you createURL('/'), which is the cleanest way to generate the right development prefix for React Navigation. If you're still standing up the app shell, this Expo getting started walkthrough is a useful baseline before layering in deep links.

Bare React Native native registration

Bare React Native needs the same concepts, but you wire more of it yourself.

On iOS, register a custom scheme in Info.plist:

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>myapp</string>
    </array>
  </dict>
</array>

On Android, register an intent filter in AndroidManifest.xml under your main activity:

<intent-filter>
  <action android:name="android.intent.action.VIEW" />
  <category android:name="android.intent.category.DEFAULT" />
  <category android:name="android.intent.category.BROWSABLE" />
  <data android:scheme="myapp" />
</intent-filter>

When the app launches from a killed state, use Linking.getInitialURL() to recover the URL that started the process:

import { useEffect } from 'react';
import { Linking } from 'react-native';

useEffect(() => {
  async function handleLaunchLink() {
    const url = await Linking.getInitialURL();

    if (url) {
      console.log('Initial deep link:', url);
      // parse and store for navigation or post-login redirect
    }
  }

  handleLaunchLink();
}, []);

If you skip getInitialURL(), your deep links may work while the app is open and still fail for the state users hit most often: cold launch from a link tap.

Also add error handling. Malformed, expired, or incomplete links shouldn't dump users into a broken route. Send them to a safe fallback screen with an explanation, not a silent redirect to home.

Configuring Universal Links and Android App Links

A flowchart showing the five steps of configuring Universal and App Links for mobile application deep linking.A flowchart showing the five steps of configuring Universal and App Links for mobile application deep linking.

What the operating systems are verifying

Universal Links and App Links work because the operating system trusts that your app is allowed to claim a specific domain. That trust doesn't come from React Native. It comes from files you host on your server plus native app configuration that points back to that domain.

Many deep linking React Native guides often get too shallow. They show a linking object and stop. The browser-to-app handoff won't be reliable until the domain is verified.

The verification chain has a few essential pieces:

PlatformVerification fileHost locationPurpose
iOSapple-app-site-association/.well-known/Tells iOS which app can open matching paths
Androidassetlinks.json/.well-known/Tells Android which package and certificate can claim the domain

The reason this matters is practical, not academic. Production apps that rely on Universal Links and App Links need those files plus correctly configured prefixes in React Navigation, or links often open the wrong place or fail to open the app at all. The same source that frames these as foundational also ties them to campaign benchmarks such as 2 to 3 percent CTR, 10 to 15 percent conversion, and 30-day retention above 20 percent for deep-linked entry points (MoldStud on advanced deep linking techniques).

iOS AASA file and Android asset links file

For iOS, host an apple-app-site-association file with no extension. A simplified example looks like this:

{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "TEAMID.com.example.myapp",
        "paths": ["/products/*", "/profile/*", "/reset-password/*"]
      }
    ]
  }
}

For Android, host assetlinks.json:

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.myapp",
      "sha256_cert_fingerprints": [
        "YOUR_RELEASE_CERT_SHA256"
      ]
    }
  }
]

A few details matter more than people expect:

  • Use the .well-known path: both platforms commonly expect these files there.
  • Serve valid JSON with the right content type: don't let your hosting layer return HTML, redirects, or attachment headers.
  • Match production identifiers exactly: bundle ID, package name, team ID, and certificate fingerprint must line up with the build users install.

Host these files as if they were part of your app binary. A tiny mismatch on the server side can invalidate perfect client code.

Server mistakes that break valid app code

The deepest frustration in this setup is that the app code can be correct while the domain still fails verification.

These are the failures I see most often:

  • Redirects in front of .well-known: the platform wants the file, not a marketing redirect or rewritten route.
  • Staging values in production files: package names and app identifiers drift easily between environments.
  • Wrong SSL or incomplete certificate chain: the URL loads in a browser, but the OS still won't trust it.
  • Overly broad path assumptions: you configured /products/*, then expected /promo/* to open too.

When links keep opening the website instead of the app, don't start in React Navigation. Start with the hosted verification files and native capabilities, then work inward.

Integrating Deep Links with React Navigation

React Navigation is where URLs become screens, params, and nested state. If your server verification is the passport check, the linking object is the route planner.

A linking object that maps to real screens

Keep your URL map readable. If marketing, support, or backend teams can't understand the paths, you'll end up debugging human inconsistency as much as code inconsistency.

import { NavigationContainer } from '@react-navigation/native';

const linking = {
  prefixes: ['https://example.com', 'myapp://'],
  config: {
    screens: {
      Root: {
        screens: {
          Home: '',
          Profile: 'profile',
          Product: 'products/:id',
          Cart: 'cart',
          Invite: 'invite/:token',
        },
      },
      NotFound: '*',
    },
  },
};

export function AppNavigation() {
  return (
    <NavigationContainer linking={linking}>
      {/* app navigators */}
    </NavigationContainer>
  );
}

That config does three useful things.

First, it keeps both HTTPS links and the custom-scheme fallback active. Second, it maps route params like :id and :token into screen params. Third, it gives you a catch-all route instead of an unhandled failure.

If you need a refresher on how route structure, nested stacks, and tabs fit together before adding deep links, this React Native navigation guide helps clarify the moving pieces.

Nested navigators and parameter parsing

Nested navigators are where shallow tutorials usually fall apart. A tab navigator inside a stack, or an auth stack wrapping an app stack, changes how paths resolve.

A more realistic setup looks like this:

const linking = {
  prefixes: ['https://example.com', 'myapp://'],
  config: {
    screens: {
      Main: {
        screens: {
          FeedTab: {
            screens: {
              FeedHome: 'feed',
              Product: 'products/:id',
            },
          },
          AccountTab: {
            screens: {
              AccountHome: 'account',
              Orders: 'orders',
            },
          },
        },
      },
      Auth: {
        screens: {
          SignIn: 'sign-in',
        },
      },
    },
  },
};

Inside a screen, pull params from route.params the same way you would for normal navigation:

function ProductScreen({ route }) {
  const { id } = route.params;

  return null;
}

For query strings, be careful. In real projects, URL parsing can get messy, especially if a provider appends campaign params, signed tokens, or redirect fragments. Normalize the incoming URL before you treat it as trusted navigation input. A path match should get you to the correct screen. Validation should decide whether the params are usable.

Programmatic deep linking with useLinkTo

useLinkTo() is useful when you want app code to speak in paths instead of route names. That becomes handy when web and mobile share conceptual routes, or when analytics and navigation should use the same canonical path.

import { useLinkTo } from '@react-navigation/native';

function PromoBanner() {
  const linkTo = useLinkTo();

  return (
    <Button
      title="Open deal"
      onPress={() => linkTo('/products/sku_123')}
    />
  );
}

That pattern also makes internal redirection cleaner. A push notification handler, an in-app message, or a saved post-login redirect can all resolve to a path rather than hard-coded navigator actions.

A few trade-offs are worth calling out:

  • Path-based navigation is easier to share across platforms. It mirrors the same routes your web app or campaign links may already use.
  • Route-name navigation is easier to refactor internally. If your paths are public, changing them has compatibility costs.
  • Overloading a single route with too many optional params gets brittle fast. It's better to support a few stable URLs than one magical parser that accepts everything.

Clean deep-link paths are part API contract, part UX contract. Treat them like public interfaces.

Advanced Patterns for Auth and Testing

A five-step flowchart illustrating the deep link authentication and testing workflow for mobile applications.A five-step flowchart illustrating the deep link authentication and testing workflow for mobile applications.

Authentication is where deep linking React Native setups stop being mechanical and start being architectural. Public routes are easy. Protected routes create a timing problem. The app receives a valid link, but the current user session can't access the target yet.

Store the target route before login

The production-safe pattern is simple. Parse the incoming link early, decide whether the target needs auth, and if the user isn't signed in, store the intended route before redirecting to login.

A lightweight version looks like this:

import { useEffect, useState } from 'react';
import { Linking } from 'react-native';

export function usePendingDeepLink(isSignedIn: boolean) {
  const [pendingUrl, setPendingUrl] = useState<string | null>(null);

  useEffect(() => {
    async function readInitialUrl() {
      const url = await Linking.getInitialURL();
      if (url && !isSignedIn) {
        setPendingUrl(url);
      }
    }

    readInitialUrl();

    const sub = Linking.addEventListener('url', ({ url }) => {
      if (!isSignedIn) {
        setPendingUrl(url);
      }
    });

    return () => sub.remove();
  }, [isSignedIn]);

  return { pendingUrl, clearPendingUrl: () => setPendingUrl(null) };
}

Then after login succeeds, redirect to the stored destination:

useEffect(() => {
  if (isSignedIn && pendingUrl) {
    // parse pendingUrl and navigate
    clearPendingUrl();
  }
}, [isSignedIn, pendingUrl]);

This pattern avoids one of the most frustrating bugs in mobile auth flows. The app correctly notices the link, sends the user to Sign In, and then forgets why they came.

If your auth layer is built on Supabase, session restoration and post-login handoff are easier when auth state is centralized. This Supabase auth guide for React Native is a good reference for organizing that side cleanly before adding redirect memory.

Don't store “go to Profile.” Store the original URL. Re-parse it after auth so one code path handles both logged-in and logged-out launches.

Test every app state on both platforms

You can't call a deep-link setup done until you've tested the app in the states users hit:

  • Foreground: app already open, current screen active
  • Background: app alive but not visible
  • Killed state: app not running at all
  • Authenticated and unauthenticated variants: especially for protected routes

That app-state matrix is part of why deep linking takes longer than it looks in ticket estimates. One path can behave correctly in foreground and fail completely on cold launch.

For manual simulation, use platform tools to open links directly:

xcrun simctl openurl booted "https://example.com/products/123"
adb shell am start -W -a android.intent.action.VIEW -d "https://example.com/products/123" com.example.myapp

Run those tests against plain public screens, protected screens, expired links, and malformed links. Also test with the app not installed so you can confirm the website fallback works as expected.

Analytics driven deep linking

Deep links aren't just routing input anymore. They're part of acquisition measurement.

One source notes that most tutorials still treat deep links as static navigation, while only 15 percent of React Navigation docs cover tracking effectiveness or handling malformed and expired links in production. The same source also states that apps using deferred deep linking with analytics integration saw 25 percent higher conversion from paid campaigns compared with basic universal links alone (OneUptime on React Native deep linking). Treat those as source-reported 2025 to 2026 findings, not universal guarantees.

A practical pattern is:

  1. Parse the URL.
  2. Extract campaign context if present.
  3. Route only after validation.
  4. Log an event with the normalized path and source metadata.
  5. Record whether auth interruption happened.

That gives product and growth teams a way to answer a better question than “Did the app open?” They can ask whether the link delivered the user to the intended content and whether auth, stale params, or bad verification broke the journey.

Common Pitfalls and Debugging Strategies

A checklist of six common troubleshooting steps to fix issues with mobile app deep linking configuration.A checklist of six common troubleshooting steps to fix issues with mobile app deep linking configuration.

Deep-link failures feel random until you debug them in the same order every time. Most of them are deterministic. The problem is usually that the failure appears in one layer while the cause lives in another.

Symptoms that usually point to config drift

If the link opens the browser instead of the app, suspect domain verification before you suspect React Navigation.

If the app opens but lands on the wrong screen, suspect the linking.config path map or your auth gate.

If custom scheme links work and HTTPS links don't, that usually means the app can parse routes but the operating system hasn't associated the domain correctly.

Here's the troubleshooting checklist I use:

  • Check the exact URL first. Verify scheme, host, path, and trailing slash behavior.
  • Confirm the hosted association files. Fetch the iOS and Android files directly and inspect the raw response.
  • Verify production identifiers. Bundle ID, package name, team ID, and signing fingerprint must match the installed build.
  • Test auth interruption explicitly. Open a protected route while signed out, then complete login.
  • Clear caches when needed. Devices and browsers can hold onto stale association state.
  • Use platform logs. Xcode logs, Android log output, and browser inspection usually reveal whether the handoff failed before or after app launch.

A walkthrough is easier to follow on video when you're in the weeds, so this demo is worth keeping nearby while you test:

A debugging checklist that saves hours

The biggest mistake junior developers make here is changing multiple layers at once. Don't edit the native config, the server files, and the React Navigation map in the same pass. You'll lose the ability to isolate the fault.

Use a fixed order instead:

  1. Validate the public URL behavior
  2. Validate domain association
  3. Validate native app registration
  4. Validate React Navigation path mapping
  5. Validate auth resume logic
  6. Validate analytics and error handling

When you work that way, deep linking stops feeling mysterious. It becomes a chain of checks. One broken link in the chain is enough to fail the experience, but each link is testable.


If you want a faster starting point for shipping production React Native apps with Expo, auth, navigation, and the surrounding app infrastructure already wired up, AppLighter gives you a strong baseline so you can spend more time on product-specific deep link flows and less time rebuilding the foundation.

Stay Updated on the Latest UI Templates and Features

Be the first to know about new React Native UI templates and kits, features, special promotions and exclusive offers by joining our newsletter.