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:
- The OS (iOS or Android) intercepts the link
- The OS checks if any installed app has registered to handle that URL
- If your app is registered, the OS launches it and passes the URL
- React Native's
Linkingmodule receives the URL - 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.
Step 1: iOS Configuration (Universal Links)
Associated Domains Entitlement
Open your iOS project in Xcode (ios/YourApp.xcworkspace).
- Select your app target
- Go to Signing & Capabilities
- Click "+ Capability" and add "Associated Domains"
- 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
Step 2: Android Configuration (App Links)
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.
Digital Asset Links
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).
Step 3: JavaScript Link Handling
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:
- Cold start: The app was not running. The user taps a link, and the app launches. Use
Linking.getInitialURL()to get the URL. - 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):
- Open Notes, paste
https://go.yourapp.com/product/123, and tap the link - Send the link via iMessage and tap it
- 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 dto 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.