{"id":893,"date":"2026-04-24T09:00:00","date_gmt":"2026-04-24T14:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=893"},"modified":"2026-03-07T03:48:33","modified_gmt":"2026-03-07T08:48:33","slug":"react-navigation-deep-links","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/react-navigation-deep-links\/","title":{"rendered":"React Navigation Deep Links: Configuration Guide"},"content":{"rendered":"\n<p>React Navigation is the standard navigation library for React Native. Its deep linking integration maps URLs directly to your navigation state, so a URL like <code>\/product\/123<\/code> opens the Product screen with <code>id: &quot;123&quot;<\/code> as a parameter. This guide covers configuration for React Navigation v6 and v7.<\/p>\n\n\n\n<p>For the native platform setup (AppDelegate, AndroidManifest), see <a href=\"https:\/\/tolinku.com\/blog\/react-native-deep-linking-setup\/\">React Native Deep Linking: Complete Setup Tutorial<\/a>. For a cross-platform overview, see <a href=\"https:\/\/tolinku.com\/blog\/cross-platform-deep-linking-guide\/\">Cross-Platform Deep Linking Guide<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Basic Linking Configuration<\/h2>\n\n\n\n<p>React Navigation&#39;s <code>linking<\/code> prop on <code>NavigationContainer<\/code> connects URLs to screens.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Define the Linking Config<\/h3>\n\n\n\n<pre><code class=\"language-javascript\">import * as Linking from &#39;expo-linking&#39;;\n\nconst linking = {\n  prefixes: [\n    &#39;https:\/\/go.yourapp.com&#39;,       \/\/ Universal Links \/ App Links\n    &#39;yourapp:\/\/&#39;,                     \/\/ Custom URL scheme\n  ],\n  config: {\n    screens: {\n      Home: &#39;&#39;,\n      Product: &#39;product\/:id&#39;,\n      Category: &#39;category\/:slug&#39;,\n      Profile: &#39;profile\/:username&#39;,\n      Settings: &#39;settings&#39;,\n      NotFound: &#39;*&#39;,\n    },\n  },\n};\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Apply to NavigationContainer<\/h3>\n\n\n\n<pre><code class=\"language-javascript\">function App() {\n  return (\n    &lt;NavigationContainer linking={linking} fallback={&lt;LoadingScreen \/&gt;}&gt;\n      &lt;Stack.Navigator&gt;\n        &lt;Stack.Screen name=&quot;Home&quot; component={HomeScreen} \/&gt;\n        &lt;Stack.Screen name=&quot;Product&quot; component={ProductScreen} \/&gt;\n        &lt;Stack.Screen name=&quot;Category&quot; component={CategoryScreen} \/&gt;\n        &lt;Stack.Screen name=&quot;Profile&quot; component={ProfileScreen} \/&gt;\n        &lt;Stack.Screen name=&quot;Settings&quot; component={SettingsScreen} \/&gt;\n        &lt;Stack.Screen name=&quot;NotFound&quot; component={NotFoundScreen} \/&gt;\n      &lt;\/Stack.Navigator&gt;\n    &lt;\/NavigationContainer&gt;\n  );\n}\n<\/code><\/pre>\n\n\n\n<p>The <code>fallback<\/code> prop shows a loading screen while React Navigation resolves the initial deep link. Without it, users may briefly see the home screen before being redirected.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Access Parameters in Screens<\/h3>\n\n\n\n<pre><code class=\"language-javascript\">function ProductScreen({ route }) {\n  const { id } = route.params;\n\n  return &lt;Text&gt;Product ID: {id}&lt;\/Text&gt;;\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Nested Navigators<\/h2>\n\n\n\n<p>Real apps have nested navigators: a tab navigator containing stack navigators. Deep linking configuration must mirror this nesting.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Example: Tabs with Nested Stacks<\/h3>\n\n\n\n<pre><code class=\"language-javascript\">\/\/ Navigation structure:\n\/\/ TabNavigator\n\/\/   \u251c\u2500\u2500 HomeTab (Stack)\n\/\/   \u2502     \u251c\u2500\u2500 Home\n\/\/   \u2502     \u2514\u2500\u2500 Product\n\/\/   \u251c\u2500\u2500 SearchTab (Stack)\n\/\/   \u2502     \u251c\u2500\u2500 Search\n\/\/   \u2502     \u2514\u2500\u2500 SearchResults\n\/\/   \u2514\u2500\u2500 ProfileTab (Stack)\n\/\/         \u251c\u2500\u2500 Profile\n\/\/         \u2514\u2500\u2500 Settings\n\nconst linking = {\n  prefixes: [&#39;https:\/\/go.yourapp.com&#39;, &#39;yourapp:\/\/&#39;],\n  config: {\n    screens: {\n      Tabs: {\n        screens: {\n          HomeTab: {\n            screens: {\n              Home: &#39;&#39;,\n              Product: &#39;product\/:id&#39;,\n            },\n          },\n          SearchTab: {\n            screens: {\n              Search: &#39;search&#39;,\n              SearchResults: &#39;search\/results&#39;,\n            },\n          },\n          ProfileTab: {\n            screens: {\n              Profile: &#39;profile\/:username&#39;,\n              Settings: &#39;settings&#39;,\n            },\n          },\n        },\n      },\n      NotFound: &#39;*&#39;,\n    },\n  },\n};\n<\/code><\/pre>\n\n\n\n<p>When a user opens <code>https:\/\/go.yourapp.com\/product\/123<\/code>, React Navigation:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Navigates to the <code>Tabs<\/code> navigator<\/li>\n<li>Selects the <code>HomeTab<\/code> tab<\/li>\n<li>Pushes the <code>Product<\/code> screen with <code>{ id: &quot;123&quot; }<\/code><\/li>\n<\/ol>\n\n\n\n<p>The user sees the Product screen with the HomeTab selected in the tab bar.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Query Parameters<\/h2>\n\n\n\n<p>Handle query parameters alongside path parameters:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">const linking = {\n  config: {\n    screens: {\n      Search: {\n        path: &#39;search&#39;,\n        parse: {\n          query: (query) =&gt; query || &#39;&#39;,\n          sort: (sort) =&gt; sort || &#39;relevance&#39;,\n        },\n      },\n    },\n  },\n};\n<\/code><\/pre>\n\n\n\n<p>URL: <code>https:\/\/go.yourapp.com\/search?query=shoes&amp;sort=price<\/code><\/p>\n\n\n\n<pre><code class=\"language-javascript\">function SearchScreen({ route }) {\n  const { query, sort } = route.params;\n  \/\/ query = &quot;shoes&quot;, sort = &quot;price&quot;\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Custom Parameter Parsing<\/h2>\n\n\n\n<p>By default, path parameters are strings. Use <code>parse<\/code> and <code>stringify<\/code> to transform them:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">const linking = {\n  config: {\n    screens: {\n      Product: {\n        path: &#39;product\/:id&#39;,\n        parse: {\n          id: Number,  \/\/ Convert string &quot;123&quot; to number 123\n        },\n        stringify: {\n          id: (id) =&gt; String(id),  \/\/ Convert back for URL generation\n        },\n      },\n      DateFilter: {\n        path: &#39;events\/:date&#39;,\n        parse: {\n          date: (date) =&gt; new Date(date),\n        },\n        stringify: {\n          date: (date) =&gt; date.toISOString().split(&#39;T&#39;)[0],\n        },\n      },\n    },\n  },\n};\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Handling Authentication Guards<\/h2>\n\n\n\n<p>A common scenario: a deep link points to a screen that requires authentication. The user taps the link but isn&#39;t logged in.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Approach 1: Redirect After Login<\/h3>\n\n\n\n<p>Store the intended deep link and redirect after authentication:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">function App() {\n  const [isLoggedIn, setIsLoggedIn] = useState(false);\n  const [pendingDeepLink, setPendingDeepLink] = useState(null);\n\n  const linking = {\n    prefixes: [&#39;https:\/\/go.yourapp.com&#39;],\n    config: {\n      screens: isLoggedIn\n        ? {\n            Home: &#39;&#39;,\n            Product: &#39;product\/:id&#39;,\n            Profile: &#39;profile\/:username&#39;,\n          }\n        : {\n            Login: &#39;*&#39;,  \/\/ All links go to Login when not authenticated\n          },\n    },\n    getStateFromPath: (path, options) =&gt; {\n      if (isLoggedIn === false) {\n        \/\/ Save the path for after login\n        setPendingDeepLink(path);\n      }\n      return undefined; \/\/ Use default behavior\n    },\n  };\n\n  \/\/ After login, navigate to the saved deep link\n  useEffect(() =&gt; {\n    if (isLoggedIn &amp;&amp; pendingDeepLink) {\n      navigation.navigate(pendingDeepLink);\n      setPendingDeepLink(null);\n    }\n  }, [isLoggedIn]);\n\n  return (\n    &lt;NavigationContainer linking={linking}&gt;\n      {\/* ... *\/}\n    &lt;\/NavigationContainer&gt;\n  );\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Approach 2: Conditional Screens<\/h3>\n\n\n\n<p>Use React Navigation&#39;s conditional rendering:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">&lt;Stack.Navigator&gt;\n  {isLoggedIn ? (\n    &lt;&gt;\n      &lt;Stack.Screen name=&quot;Home&quot; component={HomeScreen} \/&gt;\n      &lt;Stack.Screen name=&quot;Product&quot; component={ProductScreen} \/&gt;\n      &lt;Stack.Screen name=&quot;Profile&quot; component={ProfileScreen} \/&gt;\n    &lt;\/&gt;\n  ) : (\n    &lt;&gt;\n      &lt;Stack.Screen name=&quot;Login&quot; component={LoginScreen} \/&gt;\n      &lt;Stack.Screen name=&quot;Register&quot; component={RegisterScreen} \/&gt;\n    &lt;\/&gt;\n  )}\n&lt;\/Stack.Navigator&gt;\n<\/code><\/pre>\n\n\n\n<p>React Navigation handles this gracefully: if a deep link targets a screen that doesn&#39;t exist in the current navigator state, it falls through to the wildcard or default screen.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Advanced: Custom getStateFromPath<\/h2>\n\n\n\n<p>For complex routing logic that the declarative config can&#39;t handle, override <code>getStateFromPath<\/code>:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">import { getStateFromPath as defaultGetStateFromPath } from &#39;@react-navigation\/native&#39;;\n\nconst linking = {\n  prefixes: [&#39;https:\/\/go.yourapp.com&#39;],\n  config: {\n    screens: {\n      Home: &#39;&#39;,\n      Product: &#39;product\/:id&#39;,\n    },\n  },\n  getStateFromPath: (path, options) =&gt; {\n    \/\/ Custom logic: redirect legacy paths\n    if (path.startsWith(&#39;\/item\/&#39;)) {\n      path = path.replace(&#39;\/item\/&#39;, &#39;\/product\/&#39;);\n    }\n\n    \/\/ Custom logic: extract campaign tracking\n    const url = new URL(&#39;https:\/\/go.yourapp.com&#39; + path);\n    const utm_source = url.searchParams.get(&#39;utm_source&#39;);\n    if (utm_source) {\n      trackCampaignOpen(utm_source);\n    }\n\n    \/\/ Fall back to default parsing\n    return defaultGetStateFromPath(path, options);\n  },\n};\n<\/code><\/pre>\n\n\n\n<p>This is useful for:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Redirecting old URL patterns to new ones<\/li>\n<li>Extracting tracking parameters before navigation<\/li>\n<li>Adding analytics events on deep link entry<\/li>\n<li>Handling A\/B test routing<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Debugging<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Enable Debug Logging<\/h3>\n\n\n\n<p>React Navigation has built-in deep link debugging. Add the <code>onStateChange<\/code> callback:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">&lt;NavigationContainer\n  linking={linking}\n  onStateChange={(state) =&gt; {\n    console.log(&#39;Navigation state:&#39;, JSON.stringify(state, null, 2));\n  }}\n&gt;\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Common Debug Scenarios<\/h3>\n\n\n\n<p><strong>Link opens the wrong screen<\/strong>: Check that your path pattern matches the URL. Remember that paths are matched in order, and the first match wins. A wildcard <code>*<\/code> at the top of your config would match everything.<\/p>\n\n\n\n<p><strong>Parameters are undefined<\/strong>: Verify the parameter name in your path pattern matches what you access in <code>route.params<\/code>. <code>product\/:id<\/code> gives you <code>route.params.id<\/code>, not <code>route.params.productId<\/code>.<\/p>\n\n\n\n<p><strong>Nested screen not reached<\/strong>: Verify the nesting in your linking config matches your navigator nesting exactly. If the navigator structure is <code>Tabs &gt; HomeStack &gt; Product<\/code>, your config must be <code>{ Tabs: { screens: { HomeStack: { screens: { Product: &#39;product\/:id&#39; } } } } }<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Testing<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Unit Testing Link Config<\/h3>\n\n\n\n<p>Test your linking configuration without running the full app:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">import { getStateFromPath } from &#39;@react-navigation\/native&#39;;\n\ntest(&#39;product deep link resolves correctly&#39;, () =&gt; {\n  const state = getStateFromPath(&#39;\/product\/123&#39;, linking.config);\n  expect(state.routes[0].name).toBe(&#39;Product&#39;);\n  expect(state.routes[0].params.id).toBe(&#39;123&#39;);\n});\n\ntest(&#39;unknown path resolves to NotFound&#39;, () =&gt; {\n  const state = getStateFromPath(&#39;\/unknown\/path&#39;, linking.config);\n  expect(state.routes[0].name).toBe(&#39;NotFound&#39;);\n});\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Integration Testing<\/h3>\n\n\n\n<p>Use <a href=\"https:\/\/reactnavigation.org\/docs\/testing\/\" rel=\"nofollow noopener\" target=\"_blank\">React Navigation&#39;s testing utilities<\/a> to test deep link handling end-to-end in your test suite.<\/p>\n\n\n\n<p>For the Tolinku SDK integration, see the <a href=\"https:\/\/tolinku.com\/docs\/developer\/sdks\/react-native\/\">React Native SDK docs<\/a>. For routing patterns, see <a href=\"https:\/\/tolinku.com\/blog\/deep-link-routing-guide\/\">Deep Link Routing Guide<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Configure deep links with React Navigation v6+. Map URLs to screens, handle nested navigators, and pass parameters through deep links.<\/p>\n","protected":false},"author":2,"featured_media":892,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"React Navigation Deep Links: Configuration Guide","rank_math_description":"Configure deep links with React Navigation v6+. Map URLs to screens, handle nested navigators, and pass parameters through deep links.","rank_math_focus_keyword":"React Navigation deep links","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-navigation-deep-links.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-navigation-deep-links.png","footnotes":""},"categories":[15],"tags":[25,20,24,71,69,56,182,183],"class_list":["post-893","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-engineering","tag-android","tag-deep-linking","tag-ios","tag-javascript","tag-mobile-development","tag-react-native","tag-react-navigation","tag-routing"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/893","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=893"}],"version-history":[{"count":1,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/893\/revisions"}],"predecessor-version":[{"id":894,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/893\/revisions\/894"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/892"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=893"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=893"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=893"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}