{"id":1903,"date":"2026-07-31T17:00:00","date_gmt":"2026-07-31T22:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=1903"},"modified":"2026-03-07T03:50:19","modified_gmt":"2026-03-07T08:50:19","slug":"universal-links-react-native","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/universal-links-react-native\/","title":{"rendered":"Handling Universal Links in React Native iOS"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">React Native apps on iOS receive Universal Links through the same native APIs as any iOS app. The URL arrives at the AppDelegate (or SceneDelegate), gets bridged to the JavaScript layer via React Native&#39;s <code>Linking<\/code> module, and then your JavaScript router handles navigation. This article focuses on the iOS-specific configuration and common pitfalls.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For the full React Native deep linking setup (iOS and Android), see <a href=\"https:\/\/tolinku.com\/blog\/react-native-universal-links\/\">Universal Links in React Native: complete guide<\/a>. For SwiftUI-based Universal Links, see <a href=\"https:\/\/tolinku.com\/blog\/universal-links-with-swiftui\/\">Universal Links with SwiftUI: implementation guide<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Prerequisites<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>React Native 0.73+ (New Architecture compatible)<\/li>\n<li>An Apple Developer account<\/li>\n<li>An HTTPS domain you control<\/li>\n<li>AASA file hosted at <code>https:\/\/yourdomain.com\/.well-known\/apple-app-site-association<\/code><\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: Associated Domains Entitlement<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In Xcode, open the <code>ios\/<\/code> workspace:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Select the 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 <code>applinks:yourdomain.com<\/code>.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">This creates or updates the <code>.entitlements<\/code> file:<\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;key&gt;com.apple.developer.associated-domains&lt;\/key&gt;\n&lt;array&gt;\n    &lt;string&gt;applinks:yourdomain.com&lt;\/string&gt;\n&lt;\/array&gt;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For development with a local server, you can use the <code>?mode=developer<\/code> query parameter:<\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;string&gt;applinks:yourdomain.com?mode=developer&lt;\/string&gt;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This bypasses the CDN cache for the AASA file, which is useful during development. Remove it for production builds.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: AASA File<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Host at <code>https:\/\/yourdomain.com\/.well-known\/apple-app-site-association<\/code>:<\/p>\n\n\n\n<pre><code class=\"language-json\">{\n  &quot;applinks&quot;: {\n    &quot;details&quot;: [{\n      &quot;appIDs&quot;: [&quot;TEAMID.com.yourcompany.yourapp&quot;],\n      &quot;components&quot;: [\n        { &quot;\/&quot;: &quot;\/products\/*&quot; },\n        { &quot;\/&quot;: &quot;\/offers\/*&quot; },\n        { &quot;\/&quot;: &quot;\/referral\/*&quot; }\n      ]\n    }]\n  }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The AASA file must be served with:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Content-Type: <code>application\/json<\/code><\/li>\n<li>No redirects (Apple fetches from the exact URL)<\/li>\n<li>Valid HTTPS certificate<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Apple&#39;s CDN caches the AASA file. After changes, it can take up to 24 hours (or longer on some devices) to refresh. See <a href=\"https:\/\/developer.apple.com\/documentation\/bundleresources\/applinks\" rel=\"nofollow noopener\" target=\"_blank\">Apple&#39;s supporting associated domains documentation<\/a> for details.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 3: AppDelegate Configuration<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">React Native projects can use either Objective-C or Swift for the AppDelegate. Both approaches work.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Objective-C AppDelegate<\/h3>\n\n\n\n<pre><code class=\"language-objectivec\">\/\/ AppDelegate.mm\n#import &lt;React\/RCTLinkingManager.h&gt;\n\n\/\/ Universal Links\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<h3 class=\"wp-block-heading\">Swift AppDelegate<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If your project uses a Swift AppDelegate (common in newer React Native versions):<\/p>\n\n\n\n<pre><code class=\"language-swift\">\/\/ AppDelegate.swift\nimport React\n\noverride func application(\n    _ application: UIApplication,\n    continue userActivity: NSUserActivity,\n    restorationHandler: @escaping ([UIUserActivityRestoring]?) -&gt; Void\n) -&gt; Bool {\n    return RCTLinkingManager.application(\n        application,\n        continue: userActivity,\n        restorationHandler: restorationHandler\n    )\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">With SceneDelegate<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If your app uses scenes (iOS 13+), add the handler to <code>SceneDelegate<\/code> instead:<\/p>\n\n\n\n<pre><code class=\"language-swift\">\/\/ SceneDelegate.swift\nfunc scene(\n    _ scene: UIScene,\n    continue userActivity: NSUserActivity\n) {\n    guard userActivity.activityType == NSUserActivityType.browsingWeb,\n          let url = userActivity.webPageUrl else {\n        return\n    }\n\n    \/\/ Bridge to React Native&#39;s Linking module\n    RCTLinkingManager.application(\n        UIApplication.shared,\n        continue: userActivity,\n        restorationHandler: { _ in }\n    )\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If you are using both AppDelegate and SceneDelegate, the SceneDelegate method takes priority on iOS 13+.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 4: JavaScript Handling<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">React Native&#39;s <code>Linking<\/code> module receives URLs from the native bridge:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">import { Linking, Platform } from &#39;react-native&#39;;\nimport { useEffect } from &#39;react&#39;;\nimport { useNavigation } from &#39;@react-navigation\/native&#39;;\n\nfunction useUniversalLinks() {\n  const navigation = useNavigation();\n\n  useEffect(() =&gt; {\n    \/\/ Handle warm start (app already running)\n    const subscription = Linking.addEventListener(&#39;url&#39;, ({ url }) =&gt; {\n      handleUrl(url, navigation);\n    });\n\n    \/\/ Handle cold start (app launched from link)\n    Linking.getInitialURL().then(url =&gt; {\n      if (url) {\n        handleUrl(url, navigation);\n      }\n    });\n\n    return () =&gt; subscription.remove();\n  }, [navigation]);\n}\n\nfunction handleUrl(url: string, navigation: any) {\n  try {\n    const parsed = new URL(url);\n    const path = parsed.pathname;\n\n    const productMatch = path.match(\/^\\\/products\\\/([^\/]+)$\/);\n    if (productMatch) {\n      navigation.navigate(&#39;ProductDetail&#39;, {\n        productId: productMatch[1],\n        ref: parsed.searchParams.get(&#39;ref&#39;) ?? undefined\n      });\n      return;\n    }\n\n    const offerMatch = path.match(\/^\\\/offers\\\/([^\/]+)$\/);\n    if (offerMatch) {\n      navigation.navigate(&#39;OfferDetail&#39;, {\n        offerId: offerMatch[1]\n      });\n      return;\n    }\n\n    const referralMatch = path.match(\/^\\\/referral\\\/([^\/]+)$\/);\n    if (referralMatch) {\n      navigation.navigate(&#39;Referral&#39;, {\n        referrerId: referralMatch[1]\n      });\n      return;\n    }\n\n    \/\/ Unmatched URL\n    console.warn(&#39;Unmatched Universal Link:&#39;, url);\n  } catch (e) {\n    console.error(&#39;Error parsing Universal Link:&#39;, e);\n  }\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">React Navigation Deep Linking Config<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If you use <a href=\"https:\/\/reactnavigation.org\/docs\/deep-linking\/\" rel=\"nofollow noopener\" target=\"_blank\">React Navigation<\/a>, configure deep link handling declaratively:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">import { NavigationContainer, LinkingOptions } from &#39;@react-navigation\/native&#39;;\n\nconst linking: LinkingOptions&lt;RootParamList&gt; = {\n  prefixes: [&#39;https:\/\/yourdomain.com&#39;],\n  config: {\n    screens: {\n      Home: &#39;&#39;,\n      ProductDetail: &#39;products\/:productId&#39;,\n      OfferDetail: &#39;offers\/:offerId&#39;,\n      Referral: &#39;referral\/:referrerId&#39;,\n      NotFound: &#39;*&#39;\n    }\n  }\n};\n\nfunction App() {\n  return (\n    &lt;NavigationContainer linking={linking}&gt;\n      {\/* screens *\/}\n    &lt;\/NavigationContainer&gt;\n  );\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">React Navigation automatically calls <code>Linking.getInitialURL()<\/code> and subscribes to <code>Linking.addEventListener(&#39;url&#39;, ...)<\/code> when you provide a <code>linking<\/code> prop.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Nested Navigators<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">For apps with nested navigators (tabs inside a stack), map deep link paths to the nested structure:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">const linking: LinkingOptions&lt;RootParamList&gt; = {\n  prefixes: [&#39;https:\/\/yourdomain.com&#39;],\n  config: {\n    screens: {\n      MainTabs: {\n        screens: {\n          ShopTab: {\n            screens: {\n              ProductList: &#39;products&#39;,\n              ProductDetail: &#39;products\/:productId&#39;\n            }\n          },\n          DealsTab: {\n            screens: {\n              OfferList: &#39;offers&#39;,\n              OfferDetail: &#39;offers\/:offerId&#39;\n            }\n          }\n        }\n      },\n      Referral: &#39;referral\/:referrerId&#39;\n    }\n  }\n};\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Debugging<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Verify AASA File<\/h3>\n\n\n\n<pre><code class=\"language-bash\">curl -v https:\/\/yourdomain.com\/.well-known\/apple-app-site-association\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Check that:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Status is 200 (not a redirect).<\/li>\n<li>Content-Type is <code>application\/json<\/code>.<\/li>\n<li>The <code>appIDs<\/code> value matches your Team ID + bundle ID.<\/li>\n<li>The path patterns match your deep link URLs.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Test on Simulator<\/h3>\n\n\n\n<pre><code class=\"language-bash\">xcrun simctl openurl booted &quot;https:\/\/yourdomain.com\/products\/abc123&quot;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If the URL opens in Safari instead of your app:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The AASA file may not be valid or cached yet.<\/li>\n<li>The Associated Domains entitlement may be missing.<\/li>\n<li>The app may not be installed (install it first, then test).<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Check the Console<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In Xcode, filter the console for &quot;swcd&quot; to see Associated Domains daemon logs:<\/p>\n\n\n\n<pre><code>swcd: Checking app site association for yourdomain.com\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Common Issues<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Issue<\/th>\n<th>Cause<\/th>\n<th>Fix<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>Link opens Safari<\/td>\n<td>AASA not valid or not cached<\/td>\n<td>Verify AASA file, wait for cache refresh<\/td>\n<\/tr>\n<tr>\n<td><code>getInitialURL<\/code> returns null<\/td>\n<td>Missing AppDelegate\/SceneDelegate handler<\/td>\n<td>Add <code>continueUserActivity<\/code> to AppDelegate<\/td>\n<\/tr>\n<tr>\n<td>Works once then stops<\/td>\n<td>App was backgrounded, <code>addEventListener<\/code> cleaned up<\/td>\n<td>Verify the subscription is set up in <code>useEffect<\/code><\/td>\n<\/tr>\n<tr>\n<td>Works in dev but not production<\/td>\n<td><code>?mode=developer<\/code> left in entitlements<\/td>\n<td>Remove the developer mode flag<\/td>\n<\/tr>\n<tr>\n<td>Navigation happens twice<\/td>\n<td>Both AppDelegate and SceneDelegate handle the URL<\/td>\n<td>Use only SceneDelegate on iOS 13+<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Testing Checklist<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>AASA file is valid JSON and accessible via HTTPS without redirects<\/li>\n<li>Associated Domains entitlement includes <code>applinks:yourdomain.com<\/code><\/li>\n<li>AppDelegate (or SceneDelegate) forwards URLs to <code>RCTLinkingManager<\/code><\/li>\n<li><code>Linking.getInitialURL()<\/code> returns the URL on cold start<\/li>\n<li><code>Linking.addEventListener(&#39;url&#39;, ...)<\/code> fires on warm start<\/li>\n<li>React Navigation <code>linking<\/code> config matches all deep link paths<\/li>\n<li>Back navigation works correctly after deep link opens a screen<\/li>\n<li>Query parameters are passed through to the screen<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Tolinku for React Native<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/tolinku.com\/features\/deep-linking\">Tolinku<\/a> hosts AASA files automatically. Configure your iOS app details (Team ID, bundle ID) in the <a href=\"https:\/\/tolinku.com\/docs\/user-guide\/appspace-settings\/\">Appspace settings<\/a>, and the AASA file is generated and served from your domain. The <a href=\"https:\/\/tolinku.com\/docs\/developer\/sdks\/react-native\/\">React Native SDK<\/a> provides additional features including deferred deep linking and analytics.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For the full React Native deep linking guide, see <a href=\"https:\/\/tolinku.com\/blog\/react-native-universal-links\/\">Universal Links in React Native: complete guide<\/a>. For the cross-platform overview, see <a href=\"https:\/\/tolinku.com\/blog\/cross-platform-deep-linking-guide\/\">cross-platform deep linking guide for 2026<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Deep dive into Universal Links handling in React Native iOS. Configure AppDelegate, handle SceneDelegate, and bridge to JavaScript.<\/p>\n","protected":false},"author":2,"featured_media":1902,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Handling Universal Links in React Native iOS","rank_math_description":"Deep dive into Universal Links handling in React Native iOS. Configure AppDelegate, handle SceneDelegate, and bridge to JavaScript.","rank_math_focus_keyword":"universal links React Native iOS","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-universal-links-react-native.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-universal-links-react-native.png","footnotes":""},"categories":[15],"tags":[577,156,20,24,69,579,56,578,31,22],"class_list":["post-1903","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-engineering","tag-appdelegate","tag-cross-platform","tag-deep-linking","tag-ios","tag-mobile-development","tag-objective-c","tag-react-native","tag-scenedelegate","tag-swift","tag-universal-links"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1903","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=1903"}],"version-history":[{"count":1,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1903\/revisions"}],"predecessor-version":[{"id":1904,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1903\/revisions\/1904"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/1902"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=1903"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=1903"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=1903"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}