Skip to content
Tolinku
Tolinku
Sign In Start Free
Engineering · · 6 min read

React Native Deep Linking: Complete Setup Tutorial

By Tolinku Staff
|
Tolinku cross platform dashboard screenshot for engineering blog posts

Deep linking in React Native requires configuration on three levels: the native iOS project, the native Android project, and the JavaScript layer that handles incoming URLs. This guide covers all three, from initial setup to production-ready link handling.

For the navigation-specific configuration, see React Navigation Deep Links. For Universal Links details, see Universal Links: Everything You Need to Know.

How Deep Linking Works in React Native

When a user taps a link that should open your app:

  1. The OS (iOS or Android) intercepts the link
  2. The OS checks if any installed app has registered to handle that URL
  3. If your app is registered, the OS launches it and passes the URL
  4. React Native's Linking module receives the URL
  5. Your JavaScript code parses the URL and navigates to the correct screen

There are two types of links to configure:

  • Universal Links (iOS) / App Links (Android): HTTPS URLs that your app handles. These are the recommended approach because they work as regular web URLs when the app isn't installed.
  • Custom URL schemes: URLs like yourapp://product/123. Simpler to set up but less reliable and less secure.

This guide focuses on Universal Links and App Links.

Associated Domains Entitlement

Open your iOS project in Xcode (ios/YourApp.xcworkspace).

  1. Select your app target
  2. Go to Signing & Capabilities
  3. Click "+ Capability" and add "Associated Domains"
  4. Add your domain: applinks:go.yourapp.com

This tells iOS that your app handles links from go.yourapp.com.

AppDelegate Configuration

In ios/YourApp/AppDelegate.mm (or .m for older projects), add the Universal Links handler:

// Add this import at the top
#import <React/RCTLinkingManager.h>

// Add this method to your AppDelegate implementation
- (BOOL)application:(UIApplication *)application
   continueUserActivity:(NSUserActivity *)userActivity
     restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler
{
  return [RCTLinkingManager application:application
                   continueUserActivity:userActivity
                     restorationHandler:restorationHandler];
}

If you also want to handle custom URL schemes, add:

- (BOOL)application:(UIApplication *)application
   openURL:(NSURL *)url
   options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
  return [RCTLinkingManager application:application openURL:url options:options];
}

AASA File

Your deep linking platform serves the Apple App Site Association (AASA) file at https://go.yourapp.com/.well-known/apple-app-site-association. If you're using Tolinku, this is handled automatically when you configure your Appspace.

The AASA file tells iOS which URL paths your app should handle:

{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "TEAMID.com.yourapp.bundleid",
        "paths": ["/product/*", "/invite/*", "/promo/*"]
      }
    ]
  }
}

Verify it's accessible:

curl https://go.yourapp.com/.well-known/apple-app-site-association

Intent Filters

In android/app/src/main/AndroidManifest.xml, add intent filters to your main activity:

<activity
  android:name=".MainActivity"
  android:launchMode="singleTask">

  <!-- App Links (Universal Links equivalent) -->
  <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="go.yourapp.com" />
  </intent-filter>

  <!-- Existing intent filters... -->
</activity>

The android:autoVerify="true" attribute tells Android to verify domain ownership via the Digital Asset Links file.

Note the android:launchMode="singleTask". This ensures that when a link is tapped while your app is already running, it reuses the existing activity instead of creating a new one. Without this, deep links may not reach your JavaScript code correctly.

Your platform serves the assetlinks.json file at https://go.yourapp.com/.well-known/assetlinks.json. If using Tolinku, this is configured automatically.

The file links your domain to your Android app:

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.yourapp",
    "sha256_cert_fingerprints": [
      "AB:CD:EF:..."
    ]
  }
}]

Get your SHA-256 fingerprint:

keytool -list -v -keystore your-keystore.jks -alias your-alias

If you use Google Play App Signing, also include Google's signing key fingerprint from the Play Console (Setup > App signing).

Basic Linking API

React Native provides the Linking module for handling URLs:

import { Linking } from 'react-native';

// Handle link when app is already running (warm start)
useEffect(() => {
  const subscription = Linking.addEventListener('url', ({ url }) => {
    handleDeepLink(url);
  });

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

// Handle link that launched the app (cold start)
useEffect(() => {
  Linking.getInitialURL().then((url) => {
    if (url) {
      handleDeepLink(url);
    }
  });
}, []);

function handleDeepLink(url) {
  const parsed = new URL(url);
  const path = parsed.pathname;
  const params = Object.fromEntries(parsed.searchParams);

  // Route based on path
  if (path.startsWith('/product/')) {
    const productId = path.split('/')[2];
    navigation.navigate('Product', { id: productId });
  } else if (path.startsWith('/invite/')) {
    const code = path.split('/')[2];
    navigation.navigate('Invite', { code });
  }
}

Important: Cold Start vs Warm Start

There are two scenarios for receiving deep links:

  1. Cold start: The app was not running. The user taps a link, and the app launches. Use Linking.getInitialURL() to get the URL.
  2. Warm start: The app is backgrounded. The user taps a link, and the app comes to the foreground. Use Linking.addEventListener('url', ...) to receive the URL.

You must handle both. Missing either one means broken deep links in that scenario.

URL Parsing

For robust URL parsing, extract the path and query parameters:

function parseDeepLink(url) {
  try {
    const parsed = new URL(url);

    // Only handle your domain
    if (parsed.hostname !== 'go.yourapp.com') {
      return null;
    }

    return {
      path: parsed.pathname,
      params: Object.fromEntries(parsed.searchParams),
    };
  } catch {
    return null;
  }
}

This keeps your routing logic clean and separates URL parsing from navigation.

Step 4: Testing

iOS Testing

On a physical device (Universal Links don't work in the simulator for all scenarios):

  1. Open Notes, paste https://go.yourapp.com/product/123, and tap the link
  2. Send the link via iMessage and tap it
  3. Verify the app opens and navigates to the product screen

Note: typing the URL directly in Safari's address bar does NOT trigger Universal Links. You must tap a link from another context.

Android Testing

Use ADB to simulate a deep link:

adb shell am start -a android.intent.action.VIEW \
  -d "https://go.yourapp.com/product/123" \
  com.yourapp

Or paste the link in a text message or email and tap it.

Debugging

If links aren't working:

iOS:

  • Verify the AASA file is accessible from the device (not just your development machine)
  • Check that the Associated Domains entitlement matches your domain exactly
  • Delete and reinstall the app (iOS caches the AASA file on install)
  • Check the device console logs for "swcd" entries related to associated domains

Android:

  • Verify assetlinks.json is accessible
  • Check adb shell dumpsys package d to see if your app is verified for the domain
  • Verify the SHA-256 fingerprint matches your signing key (both debug and release)

For more testing tools, see Deep Link Testing Tools.

Common Pitfalls

Missing singleTask Launch Mode

Without android:launchMode="singleTask", Android may create a new activity instance when a link is tapped while the app is already running. This means onNewIntent is never called on the existing activity, and React Native's Linking event listener never fires.

Forgetting Cold Start Handling

Linking.addEventListener only fires for links received while the app is running. If the app was launched by a link, you must call Linking.getInitialURL() separately.

Testing in Development

During development, your domain's AASA/assetlinks files may not be set up yet. You can test with custom URL schemes first (they don't require server configuration), then switch to Universal Links/App Links once your domain is configured.

Expo Projects

If you're using Expo, see Expo Deep Linking: Setup Without Ejecting for the managed workflow approach.

Production Checklist

Before shipping deep links to production:

  • Associated Domains entitlement added in Xcode
  • AASA file accessible at /.well-known/apple-app-site-association
  • AppDelegate handles continueUserActivity
  • Android intent filters added with autoVerify="true"
  • assetlinks.json accessible at /.well-known/assetlinks.json
  • SHA-256 fingerprints include both debug and release (and Play App Signing)
  • Cold start handling via Linking.getInitialURL()
  • Warm start handling via Linking.addEventListener
  • URL parsing validates the domain before routing
  • Tested on physical iOS device
  • Tested on physical Android device

For integrating deep links with the Tolinku SDK, see the React Native SDK documentation. For the routing deep-dive, see Deep Link Routing Guide.

Get deep linking tips in your inbox

One email per week. No spam.

Ready to add deep linking to your app?

Set up Universal Links, App Links, deferred deep linking, and analytics in minutes. Free to start.