React Native Deep Linking: Setup, Testing & Common Mistakes to Avoid

Fernando Chaves

Fernando Chaves

August 22, 2025

Deep links turn a URL into a jump straight to the right screen. A clean react native deeplink setup shortens onboarding, boosts conversion, and connects campaigns to in-app pages without friction. Below is a practical, production-ready guide that covers schemes, iOS Universal Links, Android App Links, navigation wiring, fast testing, and the mistakes that cause most failures.

Deep Linking React Native

What deep linking is, in practice

A deep link is a URL that targets a specific route in your app. It can carry params like IDs, filters, or tabs. If the app is installed, it opens the target screen. If not, verified web links can fall back to your site, so users still land somewhere useful.

Fast path first: custom URL scheme

Custom schemes are quick to implement and useful even when you add verified links later.

Expo

Add a scheme to app.json and rebuild the dev client:

{ "expo": { "scheme": "myapp" } }

Now myapp:// links can open your app. Reference: Expo’s Linking guide is concise and accurate for this setup.

Bare React Native

Android AndroidManifest.xml

<activity
  android:name=".MainActivity"
  android:launchMode="singleTask">
  <intent-filter>
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />
  </intent-filter>

  <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>
</activity>

singleTask lets your existing activity receive new link intents.

iOS Info.plist

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

iOS AppDelegate bridge

import React

override func application(_ app: UIApplication,
  open url: URL,
  options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
  return RCTLinkingManager.application(app, open: url, options: options)
}

override func application(_ application: UIApplication,
  continue userActivity: NSUserActivity,
  restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
  return RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler)
}

Users tap https:// and iOS routes straight into your app if installed.

Steps:

  1. Add the Associated Domains entitlement, for example applinks:app.example.com.
  2. Host https://app.example.com/.well-known/apple-app-site-association with allowed paths and your app ID.
  3. Reinstall the app to trigger iOS to fetch and cache the file.

Minimal apple-app-site-association:

{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "QQ57RJ5UTD.com.example.myapp",
        "paths": [ "/product/*", "/profile/*" ]
      }
    ]
  }
}

Android verifies domain ownership and bypasses the chooser once verified.

Steps:

  1. Add an intent filter with http, https, and android:autoVerify="true".
  2. Host https://app.example.com/.well-known/assetlinks.json with your package name and SHA256 cert fingerprint.
  3. Reinstall and allow the device to verify.

Manifest example:

<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="app.example.com" />
</intent-filter>

assetlinks.json minimal:

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.app",
      "sha256_cert_fingerprints": ["14:6D:E9:83:...:FF"]
    }
  }
]
Mobile Phone

Wire the app: React Navigation and URL mapping

Whether you use Expo or bare, map URLs to screens so both the initial open and subsequent events work.

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

const prefix = Linking.createURL('/') // exp in dev, your scheme in native
const linking = {
  prefixes: [prefix, 'https://app.example.com', 'myapp://'],
  config: {
    screens: {
      Home: 'home',
      Product: 'product/:id',
      Profile: 'profile/:id?',
      Settings: 'settings'
    }
  }
}

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

Prefer one source of truth for paths. Keep your server routes and navigation map in sync.

If you need a manual hook:

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

export function useDeepLinking(onUrl: (url: string) => void) {
  useEffect(() => {
    const sub = Linking.addEventListener('url', e => onUrl(e.url))
    Linking.getInitialURL().then(u => u && onUrl(u))
    return () => sub.remove()
  }, [onUrl])
}

Use real taps and commands, not just typing in the address bar.

# iOS Simulator
xcrun simctl openurl booted "myapp://product/42"

# Android emulator or device
adb shell am start -W -a android.intent.action.VIEW -d "myapp://product/42" com.example.app

# Expo dev client or Expo Go
npx uri-scheme open "myapp://product/42" --ios
npx uri-scheme open "myapp://product/42" --android

Add these to a small script so QA can reproduce with one command.

Terminal Command

Analytics, SEO and fallback

  • Use https links for emails, ads, social, and SEO. When the app is not installed, route to the equivalent web page.
  • Capture utm_* and custom params so attribution survives the jump from web to app.
  • If a user is not authenticated, send them through a login that returns to the original deep linked screen.
MistakeSymptomFix
Missing association filesLinks open the browser instead of the appPublish a valid AASA or assetlinks.json, reinstall, re-verify
Scheme only for public trafficChooser on Android, poor UX from emailUse verified https links for campaigns
No singleTask or iOS bridgeApp running but new links are ignoredAdd singleTask, wire RCTLinkingManager in AppDelegate
Wrong prefixes or path configLink opens the app but lands on HomeEnsure prefixes and config.screens match every path
Stale verification cacheChanges appear to do nothingReinstall the app, clear cached association, verify again
Ignored query paramsMissing filters or state on openCentralize param parsing and sanitization
No error fallbackBroken entity IDs cause blank screensRoute to a safe default with context and retry options

Troubleshooting checklist

  • Confirm the scheme works first, then layer in verified https.
  • Log both Linking.getInitialURL() and the url event to see real input.
  • Curl your AASA or assetlinks.json to verify HTTPS, content type, and JSON.
  • Validate Associated Domains entitlement and android:autoVerify.
  • Map every public path in config.screens, including nested routes.
  • Test cold start and warm start, logged in and logged out, slow network and airplane mode.
  • Document the canonical link formats for marketing, QA, and support.

Wrap up

React Native Boilerplate

Deep linking connects your web, email, and mobile experience without extra taps. Ship a simple scheme for internal use, add Universal Links and App Links for public traffic, map every route, and test with real taps plus command line. Keep verification in your release checklist so links do not silently break.

Want this ready on day one, along with auth, payments, i18n, and polished UI? Use the React Native boilerplate at launchyourapp.dev. It already includes navigation, a sensible linking setup, and patterns for params and fallbacks. Wire your domain, add your screens, and focus on the features that move the needle.