{"id":884,"date":"2026-04-23T09:00:00","date_gmt":"2026-04-23T14:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=884"},"modified":"2026-03-07T03:48:32","modified_gmt":"2026-03-07T08:48:32","slug":"react-native-deep-linking-setup","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/react-native-deep-linking-setup\/","title":{"rendered":"React Native Deep Linking: Complete Setup Tutorial"},"content":{"rendered":"\n<p>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.<\/p>\n\n\n\n<p>For the navigation-specific configuration, see <a href=\"https:\/\/tolinku.com\/blog\/react-navigation-deep-links\/\">React Navigation Deep Links<\/a>. For Universal Links details, see <a href=\"https:\/\/tolinku.com\/blog\/universal-links-everything-you-need-to-know\/\">Universal Links: Everything You Need to Know<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">How Deep Linking Works in React Native<\/h2>\n\n\n\n<p>When a user taps a link that should open your app:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>The OS (iOS or Android) intercepts the link<\/li>\n<li>The OS checks if any installed app has registered to handle that URL<\/li>\n<li>If your app is registered, the OS launches it and passes the URL<\/li>\n<li>React Native&#39;s <code>Linking<\/code> module receives the URL<\/li>\n<li>Your JavaScript code parses the URL and navigates to the correct screen<\/li>\n<\/ol>\n\n\n\n<p>There are two types of links to configure:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Universal Links (iOS) \/ App Links (Android)<\/strong>: HTTPS URLs that your app handles. These are the recommended approach because they work as regular web URLs when the app isn&#39;t installed.<\/li>\n<li><strong>Custom URL schemes<\/strong>: URLs like <code>yourapp:\/\/product\/123<\/code>. Simpler to set up but less reliable and less secure.<\/li>\n<\/ul>\n\n\n\n<p>This guide focuses on Universal Links and App Links.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: iOS Configuration (Universal Links)<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Associated Domains Entitlement<\/h3>\n\n\n\n<p>Open your iOS project in Xcode (<code>ios\/YourApp.xcworkspace<\/code>).<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Select your app target<\/li>\n<li>Go to Signing &amp; Capabilities<\/li>\n<li>Click &quot;+ Capability&quot; and add &quot;Associated Domains&quot;<\/li>\n<li>Add your domain: <code>applinks:go.yourapp.com<\/code><\/li>\n<\/ol>\n\n\n\n<p>This tells iOS that your app handles links from <code>go.yourapp.com<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">AppDelegate Configuration<\/h3>\n\n\n\n<p>In <code>ios\/YourApp\/AppDelegate.mm<\/code> (or <code>.m<\/code> for older projects), add the Universal Links handler:<\/p>\n\n\n\n<pre><code class=\"language-objective-c\">\/\/ Add this import at the top\n#import &lt;React\/RCTLinkingManager.h&gt;\n\n\/\/ Add this method to your AppDelegate implementation\n- (BOOL)application:(UIApplication *)application\n   continueUserActivity:(NSUserActivity *)userActivity\n     restorationHandler:(void (^)(NSArray&lt;id&lt;UIUserActivityRestoring&gt;&gt; * _Nullable))restorationHandler\n{\n  return [RCTLinkingManager application:application\n                   continueUserActivity:userActivity\n                     restorationHandler:restorationHandler];\n}\n<\/code><\/pre>\n\n\n\n<p>If you also want to handle custom URL schemes, add:<\/p>\n\n\n\n<pre><code class=\"language-objective-c\">- (BOOL)application:(UIApplication *)application\n   openURL:(NSURL *)url\n   options:(NSDictionary&lt;UIApplicationOpenURLOptionsKey,id&gt; *)options\n{\n  return [RCTLinkingManager application:application openURL:url options:options];\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">AASA File<\/h3>\n\n\n\n<p>Your deep linking platform serves the Apple App Site Association (AASA) file at <code>https:\/\/go.yourapp.com\/.well-known\/apple-app-site-association<\/code>. If you&#39;re using Tolinku, this is handled automatically when you configure your Appspace.<\/p>\n\n\n\n<p>The AASA file tells iOS which URL paths your app should handle:<\/p>\n\n\n\n<pre><code class=\"language-json\">{\n  &quot;applinks&quot;: {\n    &quot;apps&quot;: [],\n    &quot;details&quot;: [\n      {\n        &quot;appID&quot;: &quot;TEAMID.com.yourapp.bundleid&quot;,\n        &quot;paths&quot;: [&quot;\/product\/*&quot;, &quot;\/invite\/*&quot;, &quot;\/promo\/*&quot;]\n      }\n    ]\n  }\n}\n<\/code><\/pre>\n\n\n\n<p>Verify it&#39;s accessible:<\/p>\n\n\n\n<pre><code class=\"language-bash\">curl https:\/\/go.yourapp.com\/.well-known\/apple-app-site-association\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Android Configuration (App Links)<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Intent Filters<\/h3>\n\n\n\n<p>In <code>android\/app\/src\/main\/AndroidManifest.xml<\/code>, add intent filters to your main activity:<\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;activity\n  android:name=&quot;.MainActivity&quot;\n  android:launchMode=&quot;singleTask&quot;&gt;\n\n  &lt;!-- App Links (Universal Links equivalent) --&gt;\n  &lt;intent-filter android:autoVerify=&quot;true&quot;&gt;\n    &lt;action android:name=&quot;android.intent.action.VIEW&quot; \/&gt;\n    &lt;category android:name=&quot;android.intent.category.DEFAULT&quot; \/&gt;\n    &lt;category android:name=&quot;android.intent.category.BROWSABLE&quot; \/&gt;\n    &lt;data android:scheme=&quot;https&quot; android:host=&quot;go.yourapp.com&quot; \/&gt;\n  &lt;\/intent-filter&gt;\n\n  &lt;!-- Existing intent filters... --&gt;\n&lt;\/activity&gt;\n<\/code><\/pre>\n\n\n\n<p>The <code>android:autoVerify=&quot;true&quot;<\/code> attribute tells Android to verify domain ownership via the Digital Asset Links file.<\/p>\n\n\n\n<p>Note the <code>android:launchMode=&quot;singleTask&quot;<\/code>. 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.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Digital Asset Links<\/h3>\n\n\n\n<p>Your platform serves the assetlinks.json file at <code>https:\/\/go.yourapp.com\/.well-known\/assetlinks.json<\/code>. If using Tolinku, this is configured automatically.<\/p>\n\n\n\n<p>The file links your domain to your Android app:<\/p>\n\n\n\n<pre><code class=\"language-json\">[{\n  &quot;relation&quot;: [&quot;delegate_permission\/common.handle_all_urls&quot;],\n  &quot;target&quot;: {\n    &quot;namespace&quot;: &quot;android_app&quot;,\n    &quot;package_name&quot;: &quot;com.yourapp&quot;,\n    &quot;sha256_cert_fingerprints&quot;: [\n      &quot;AB:CD:EF:...&quot;\n    ]\n  }\n}]\n<\/code><\/pre>\n\n\n\n<p>Get your SHA-256 fingerprint:<\/p>\n\n\n\n<pre><code class=\"language-bash\">keytool -list -v -keystore your-keystore.jks -alias your-alias\n<\/code><\/pre>\n\n\n\n<p>If you use Google Play App Signing, also include Google&#39;s signing key fingerprint from the Play Console (Setup &gt; App signing).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 3: JavaScript Link Handling<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Basic Linking API<\/h3>\n\n\n\n<p>React Native provides the <code>Linking<\/code> module for handling URLs:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">import { Linking } from &#39;react-native&#39;;\n\n\/\/ Handle link when app is already running (warm start)\nuseEffect(() =&gt; {\n  const subscription = Linking.addEventListener(&#39;url&#39;, ({ url }) =&gt; {\n    handleDeepLink(url);\n  });\n\n  return () =&gt; subscription.remove();\n}, []);\n\n\/\/ Handle link that launched the app (cold start)\nuseEffect(() =&gt; {\n  Linking.getInitialURL().then((url) =&gt; {\n    if (url) {\n      handleDeepLink(url);\n    }\n  });\n}, []);\n\nfunction handleDeepLink(url) {\n  const parsed = new URL(url);\n  const path = parsed.pathname;\n  const params = Object.fromEntries(parsed.searchParams);\n\n  \/\/ Route based on path\n  if (path.startsWith(&#39;\/product\/&#39;)) {\n    const productId = path.split(&#39;\/&#39;)[2];\n    navigation.navigate(&#39;Product&#39;, { id: productId });\n  } else if (path.startsWith(&#39;\/invite\/&#39;)) {\n    const code = path.split(&#39;\/&#39;)[2];\n    navigation.navigate(&#39;Invite&#39;, { code });\n  }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Important: Cold Start vs Warm Start<\/h3>\n\n\n\n<p>There are two scenarios for receiving deep links:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Cold start<\/strong>: The app was not running. The user taps a link, and the app launches. Use <code>Linking.getInitialURL()<\/code> to get the URL.<\/li>\n<li><strong>Warm start<\/strong>: The app is backgrounded. The user taps a link, and the app comes to the foreground. Use <code>Linking.addEventListener(&#39;url&#39;, ...)<\/code> to receive the URL.<\/li>\n<\/ol>\n\n\n\n<p>You must handle both. Missing either one means broken deep links in that scenario.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">URL Parsing<\/h3>\n\n\n\n<p>For robust URL parsing, extract the path and query parameters:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">function parseDeepLink(url) {\n  try {\n    const parsed = new URL(url);\n\n    \/\/ Only handle your domain\n    if (parsed.hostname !== &#39;go.yourapp.com&#39;) {\n      return null;\n    }\n\n    return {\n      path: parsed.pathname,\n      params: Object.fromEntries(parsed.searchParams),\n    };\n  } catch {\n    return null;\n  }\n}\n<\/code><\/pre>\n\n\n\n<p>This keeps your routing logic clean and separates URL parsing from navigation.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 4: Testing<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">iOS Testing<\/h3>\n\n\n\n<p>On a physical device (Universal Links don&#39;t work in the simulator for all scenarios):<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Open Notes, paste <code>https:\/\/go.yourapp.com\/product\/123<\/code>, and tap the link<\/li>\n<li>Send the link via iMessage and tap it<\/li>\n<li>Verify the app opens and navigates to the product screen<\/li>\n<\/ol>\n\n\n\n<p>Note: typing the URL directly in Safari&#39;s address bar does NOT trigger Universal Links. You must tap a link from another context.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Android Testing<\/h3>\n\n\n\n<p>Use ADB to simulate a deep link:<\/p>\n\n\n\n<pre><code class=\"language-bash\">adb shell am start -a android.intent.action.VIEW \\\n  -d &quot;https:\/\/go.yourapp.com\/product\/123&quot; \\\n  com.yourapp\n<\/code><\/pre>\n\n\n\n<p>Or paste the link in a text message or email and tap it.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Debugging<\/h3>\n\n\n\n<p>If links aren&#39;t working:<\/p>\n\n\n\n<p><strong>iOS:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Verify the AASA file is accessible from the device (not just your development machine)<\/li>\n<li>Check that the Associated Domains entitlement matches your domain exactly<\/li>\n<li>Delete and reinstall the app (iOS caches the AASA file on install)<\/li>\n<li>Check the device console logs for &quot;swcd&quot; entries related to associated domains<\/li>\n<\/ul>\n\n\n\n<p><strong>Android:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Verify assetlinks.json is accessible<\/li>\n<li>Check <code>adb shell dumpsys package d<\/code> to see if your app is verified for the domain<\/li>\n<li>Verify the SHA-256 fingerprint matches your signing key (both debug and release)<\/li>\n<\/ul>\n\n\n\n<p>For more testing tools, see <a href=\"https:\/\/tolinku.com\/blog\/deep-link-testing-tools\/\">Deep Link Testing Tools<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Common Pitfalls<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Missing <code>singleTask<\/code> Launch Mode<\/h3>\n\n\n\n<p>Without <code>android:launchMode=&quot;singleTask&quot;<\/code>, Android may create a new activity instance when a link is tapped while the app is already running. This means <code>onNewIntent<\/code> is never called on the existing activity, and React Native&#39;s <code>Linking<\/code> event listener never fires.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Forgetting Cold Start Handling<\/h3>\n\n\n\n<p><code>Linking.addEventListener<\/code> only fires for links received while the app is running. If the app was launched by a link, you must call <code>Linking.getInitialURL()<\/code> separately.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Testing in Development<\/h3>\n\n\n\n<p>During development, your domain&#39;s AASA\/assetlinks files may not be set up yet. You can test with custom URL schemes first (they don&#39;t require server configuration), then switch to Universal Links\/App Links once your domain is configured.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Expo Projects<\/h3>\n\n\n\n<p>If you&#39;re using Expo, see <a href=\"https:\/\/tolinku.com\/blog\/expo-deep-linking\/\">Expo Deep Linking: Setup Without Ejecting<\/a> for the managed workflow approach.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Production Checklist<\/h2>\n\n\n\n<p>Before shipping deep links to production:<\/p>\n\n\n\n<ul class=\"checklist wp-block-list\"><li><input type=\"checkbox\" disabled> Associated Domains entitlement added in Xcode<\/li><li><input type=\"checkbox\" disabled> AASA file accessible at <code>\/.well-known\/apple-app-site-association<\/code><\/li><li><input type=\"checkbox\" disabled> AppDelegate handles <code>continueUserActivity<\/code><\/li><li><input type=\"checkbox\" disabled> Android intent filters added with <code>autoVerify=\"true\"<\/code><\/li><li><input type=\"checkbox\" disabled> assetlinks.json accessible at <code>\/.well-known\/assetlinks.json<\/code><\/li><li><input type=\"checkbox\" disabled> SHA-256 fingerprints include both debug and release (and Play App Signing)<\/li><li><input type=\"checkbox\" disabled> Cold start handling via <code>Linking.getInitialURL()<\/code><\/li><li><input type=\"checkbox\" disabled> Warm start handling via <code>Linking.addEventListener<\/code><\/li><li><input type=\"checkbox\" disabled> URL parsing validates the domain before routing<\/li><li><input type=\"checkbox\" disabled> Tested on physical iOS device<\/li><li><input type=\"checkbox\" disabled> Tested on physical Android device<\/li><\/ul>\n\n\n\n<p>For integrating deep links with the Tolinku SDK, see the <a href=\"https:\/\/tolinku.com\/docs\/developer\/sdks\/react-native\/\">React Native SDK documentation<\/a>. For the routing deep-dive, see <a href=\"https:\/\/tolinku.com\/blog\/deep-link-routing-guide\/\">Deep Link Routing Guide<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Set up deep linking in React Native from scratch. Configure Universal Links, App Links, and navigation handling for iOS and Android.<\/p>\n","protected":false},"author":2,"featured_media":883,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"React Native Deep Linking: Complete Setup Tutorial","rank_math_description":"Set up deep linking in React Native from scratch. Configure Universal Links, App Links, and navigation handling for iOS and Android.","rank_math_focus_keyword":"React Native deep linking","rank_math_canonical_url":"","rank_math_facebook_title":"","rank_math_facebook_description":"","rank_math_facebook_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-react-native-deep-linking-setup.png","rank_math_facebook_image_id":"","rank_math_twitter_title":"","rank_math_twitter_description":"","rank_math_twitter_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-react-native-deep-linking-setup.png","footnotes":""},"categories":[15],"tags":[25,23,20,24,71,69,56,22],"class_list":["post-884","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-engineering","tag-android","tag-app-links","tag-deep-linking","tag-ios","tag-javascript","tag-mobile-development","tag-react-native","tag-universal-links"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/884","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/comments?post=884"}],"version-history":[{"count":3,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/884\/revisions"}],"predecessor-version":[{"id":2521,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/884\/revisions\/2521"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/883"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=884"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=884"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=884"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}