AboutBlogContact
Mobile DevelopmentAugust 11, 2025 8 min read 93Updated: May 3, 2026

Deep Linking in React Native: Universal Links, App Links, and Deferred Deep Links

AunimedaAunimeda
📋 Table of Contents

Deep Linking in React Native: Universal Links, App Links, and Deferred Deep Links

Deep links are URLs that open your app to a specific screen - not just the home screen. A link in an email that opens the app directly to a product page. A social share that opens to the shared post. A password reset link that opens to the reset form.

Implemented correctly, deep links improve conversion rates significantly - users who click a deep link and land on the relevant screen convert 2-3x better than users who land on the home screen and have to navigate.

Implemented incorrectly, they fail silently - the user opens the app but lands on the home screen, the link is lost, and the business value evaporates.

Here's the complete guide.


Three types of deep links

1. URI Schemes (custom URL schemes)

myapp://products/123
myapp://profile/settings

Simple, easy to implement, work on iOS and Android. Major limitation: any app can register the same URI scheme. myapp:// could be claimed by a malicious app. The system doesn't verify that myapp:// links should go to your app. For this reason, iOS and Android have introduced verified link schemes.

2. Universal Links (iOS) / App Links (Android)

https://yourapp.com/products/123
https://yourapp.com/profile/settings

These look like regular HTTPS URLs. The OS verifies that your app "owns" this domain via a well-known file on the domain. If the app is installed, the OS routes the link to the app. If not installed, it opens the browser to your web URL.

This is the correct approach for production apps.

3. Deferred Deep Links

The hardest case: user sees a deep link but doesn't have the app installed. They go to the App Store, install the app, and then open it. The deep link context is gone.

Deferred deep links preserve the original link through the install flow and deliver it on first open. This is critical for marketing campaigns, referral programs, and shareable content.


Setting up Universal Links (iOS)

Step 1: AASA file on your domain

Create https://yourapp.com/.well-known/apple-app-site-association (no .json extension):

{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "TEAMID.com.yourcompany.yourapp",
        "paths": [
          "/products/*",
          "/profile/*",
          "/orders/*",
          "/invite/*",
          "NOT /blog/*",
          "NOT /admin/*"
        ]
      }
    ]
  }
}

The TEAMID is your Apple Developer Team ID (10-character alphanumeric string found in App Store Connect). This file must be served over HTTPS with Content-Type: application/json.

Step 2: Enable Associated Domains in Xcode

In your Xcode project → Signing & Capabilities → Add Capability → Associated Domains:

applinks:yourapp.com
applinks:www.yourapp.com

Step 3: iOS app delegate

// AppDelegate.mm
- (BOOL)application:(UIApplication *)application
  continueUserActivity:(NSUserActivity *)userActivity
  restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler
{
  return [RCTLinkingManager application:application
                   continueUserActivity:userActivity
                     restorationHandler:restorationHandler];
}

Setting up App Links (Android)

Step 1: assetlinks.json on your domain

Create https://yourapp.com/.well-known/assetlinks.json:

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

Get your SHA-256 fingerprint:

# Debug keystore (for development)
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android

# Production keystore
keytool -list -v -keystore your-release-key.keystore -alias your-key-alias

Important: You need both debug and production fingerprints in the file for proper testing.

Step 2: AndroidManifest.xml

<activity
  android:name=".MainActivity"
  android:launchMode="singleTask">
  
  <!-- URI scheme (fallback) -->
  <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>
  
  <!-- App Links (verified HTTPS) -->
  <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="yourapp.com"
      android:pathPrefix="/products" />
    <data
      android:scheme="https"
      android:host="yourapp.com"
      android:pathPrefix="/profile" />
  </intent-filter>
  
</activity>

React Native: handling deep links

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

export function useDeepLinking(navigationRef: NavigationContainerRef) {
  
  useEffect(() => {
    // Handle link when app is already open (foreground)
    const subscription = Linking.addEventListener('url', ({ url }) => {
      handleDeepLink(url, navigationRef);
    });
    
    // Handle link that opened the app from background/killed state
    Linking.getInitialURL().then((url) => {
      if (url) {
        // Wait for navigation to be ready
        const interval = setInterval(() => {
          if (navigationRef.isReady()) {
            clearInterval(interval);
            handleDeepLink(url, navigationRef);
          }
        }, 100);
      }
    });
    
    return () => subscription.remove();
  }, []);
}

function handleDeepLink(url: string, navigationRef: NavigationContainerRef) {
  const parsed = parseDeepLink(url);
  if (!parsed) return;
  
  switch (parsed.screen) {
    case 'product':
      navigationRef.navigate('ProductDetail', { productId: parsed.params.id });
      break;
    case 'profile':
      navigationRef.navigate('Profile', { userId: parsed.params.userId });
      break;
    case 'order':
      // Requires authentication
      if (!isAuthenticated()) {
        // Save link, redirect to login, then navigate after login
        storePendingDeepLink(url);
        navigationRef.navigate('Login');
      } else {
        navigationRef.navigate('OrderDetail', { orderId: parsed.params.id });
      }
      break;
  }
}

function parseDeepLink(url: string): { screen: string; params: Record<string, string> } | null {
  try {
    const parsed = new URL(url);
    const path = parsed.pathname;
    
    // Handle both https://yourapp.com/products/123 and myapp://products/123
    const productMatch = path.match(/^\/products\/([^/]+)/);
    if (productMatch) return { screen: 'product', params: { id: productMatch[1] } };
    
    const profileMatch = path.match(/^\/profile\/([^/]+)/);
    if (profileMatch) return { screen: 'profile', params: { userId: profileMatch[1] } };
    
    const orderMatch = path.match(/^\/orders\/([^/]+)/);
    if (orderMatch) return { screen: 'order', params: { id: orderMatch[1] } };
    
    return null;
  } catch {
    return null;
  }
}

Deferred deep links

For deferred deep links (preserving link through install), you have two options:

Option A: Branch.io or Firebase Dynamic Links

Firebase Dynamic Links was deprecated in 2025. Branch.io is the current standard.

import branch from 'react-native-branch';

// Create a shareable deep link
async function createShareLink(productId: string, userId: string) {
  const branchUniversalObject = await branch.createBranchUniversalObject(
    `product/${productId}`,
    {
      title: 'Check out this product',
      contentImageUrl: 'https://yourapp.com/products/${productId}/image',
      contentMetadata: {
        customMetadata: {
          productId,
          referrerId: userId,
        },
      },
    }
  );
  
  const { url } = await branchUniversalObject.generateShortUrl({
    feature: 'share',
    channel: 'app',
    campaign: 'product_share',
  }, {
    $desktop_url: `https://yourapp.com/products/${productId}`,
    $ios_url: `https://yourapp.com/products/${productId}`,
  });
  
  return url; // Something like: https://yourapp.app.link/abc123
}

// On app first open, receive the deferred link
useEffect(() => {
  branch.subscribe(({ error, params }) => {
    if (error) return;
    if (!params['+clicked_branch_link']) return;
    
    // This fires on first open after install if user came from a Branch link
    const { productId, referrerId } = params;
    if (productId) {
      navigationRef.navigate('ProductDetail', { productId });
    }
    if (referrerId) {
      // Credit referral
      api.post('/referrals', { referrerId });
    }
  });
}, []);

Option B: Custom deferred deep links via fingerprinting

If you don't want a third-party service, you can implement basic deferred deep links using device fingerprinting:

// When user clicks link on web (before app install):
// Store link data in your backend associated with device fingerprint
// (IP + User-Agent hash - not reliable for 100% of cases but works for ~70%)

// On first app open:
async function checkDeferredDeepLink() {
  const deviceInfo = await getDeviceFingerprint();
  
  const response = await api.post('/deferred-deep-links/check', {
    fingerprint: deviceInfo,
    installTime: new Date().toISOString(),
  });
  
  if (response.pendingLink) {
    handleDeepLink(response.pendingLink, navigationRef);
  }
}

Fingerprinting is ~70% accurate. Branch.io is ~95%+ accurate and handles edge cases like VPN users, multiple clicks. For any growth-critical use case (referral programs, marketing campaigns), Branch or a similar service is worth the cost.


Testing Universal Links and App Links

iOS Universal Links cannot be tested in the simulator. Test on a physical device. The OS caches the AASA file aggressively - to force a re-fetch after changing it:

# Reset AASA cache on device (iOS 16+)
# Settings → Developer → Reset Universal Links Cache

# Or: hold down the link in Messages/Notes for a preview, 
# which forces a re-fetch

Android App Links verification:

# Check App Link verification status
adb shell pm get-app-links com.yourcompany.yourapp

# Expected output:
# com.yourcompany.yourapp:
#   ID: 1
#   Signatures: [SHA256_CERT_FINGERPRINT]
#   Domain verification state:
#     yourapp.com: verified  ← this is what you want

Common failure: assetlinks.json not served correctly

# Verify the file is accessible
curl -v https://yourapp.com/.well-known/assetlinks.json

# Must return:
# Content-Type: application/json
# No redirect (some servers redirect, which breaks verification)
# Status 200

If your server redirects www.yourapp.com to yourapp.com, you need the assetlinks.json accessible on BOTH domains, or App Links verification will fail for the redirected domain.


The authenticated deep link pattern

Many deep links require the user to be logged in (order status, profile edit, payment confirmation). Never drop the deep link if the user is not authenticated:

// 1. Store the pending deep link before redirecting to login
function navigateToDeepLink(url: string) {
  if (requiresAuth(url) && !store.getState().auth.isLoggedIn) {
    store.dispatch(setPendingDeepLink(url));
    navigation.navigate('Login');
    return;
  }
  handleDeepLink(url, navigation);
}

// 2. After successful login, check for pending link
function onLoginSuccess() {
  const pendingLink = store.getState().navigation.pendingDeepLink;
  if (pendingLink) {
    store.dispatch(clearPendingDeepLink());
    handleDeepLink(pendingLink, navigation);
  } else {
    navigation.navigate('Home');
  }
}

This pattern is often omitted from initial implementations and causes significant drop-off in email campaigns where users aren't logged in when they click links.


Analytics: what to track

// Track every deep link open with full attribution
analytics.track('deep_link_opened', {
  url: originalUrl,
  screen: parsedRoute.screen,
  params: parsedRoute.params,
  source: params['~channel'] || 'direct', // Branch attribution
  campaign: params['~campaign'],
  wasInstalled: !wasAlreadyInstalled, // Deferred = true
  userId: currentUserId,
});

Key metrics to monitor:

  • Deep link open rate by source (email, push, social)
  • Deferred install rate from marketing campaigns
  • Navigation success rate (did the link successfully navigate to the target screen?)
  • Drop-off at authentication wall (how many users don't complete login after a deep link?)

Deep links that silently fail to navigate correctly are a common bug - users land on the home screen, don't realize anything went wrong, and you lose the conversion. The analytics catch this.


Aunimeda develops mobile applications for iOS and Android - from MVP to production-ready apps with full backend integration.

Contact us to discuss your mobile project. See also: Mobile App Development, Mobile Game Development

Read Also

Flutter vs React Native in 2026: An Engineer's Honest Comparisonaunimeda
Mobile Development

Flutter vs React Native in 2026: An Engineer's Honest Comparison

Flutter and React Native both ship to iOS and Android from a single codebase. But they make radically different bets. Here's the concrete trade-offs - performance benchmarks, ecosystem, hiring market, and which one fits which product.

React Native Push Notifications in 2026: Complete Guide (Expo + Firebase)aunimeda
Mobile Development

React Native Push Notifications in 2026: Complete Guide (Expo + Firebase)

Push notifications are the single highest-ROI feature in mobile apps. Open rates are 7x higher than email. Here's how to implement them correctly in React Native - including background handling, deep linking, and analytics.

Flutter vs React Native in 2026 - An Honest Comparisonaunimeda
Mobile Development

Flutter vs React Native in 2026 - An Honest Comparison

Flutter or React Native for your next mobile app? We've built production apps with both. Here's what actually matters when choosing in 2026.

Need IT development for your business?

We build websites, mobile apps and AI solutions. Free consultation.

Mobile App Development

Get Consultation All articles