{"id":"l-rn-3-5","courseId":"react-native-apps","lang":"en","kind":"lesson","summary":"Present Trailhead’s add-hike form as a modal, keep signed-out users on the sign-in screen with Stack.Protected, and open specific hikes from deep links, knowing how URLs are matched to route files.","html":"<p>Three situations don’t fit the push-a-screen pattern you’ve used so far. Logging a hike is a short task the user starts and finishes, so it should feel separate from browsing. Some screens should exist only for signed-in users. And Trailhead should open on the right hike when someone taps a link in a reminder or a message. Expo Router handles all three in the layout file.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">The add-hike form as a modal</h2>\n<p>A modal is a screen presented over the current context, usually sliding up from the bottom. On iOS it appears as a card with the previous screen visible behind it, and users can swipe it down to cancel. Make the form a modal by setting its presentation in the root Stack:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">src/app/_layout.tsx</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack.Screen</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"(tabs)\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> options</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ headerShown: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> /></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack.Screen</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"hikes/[id]\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> options</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ title: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Hike'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> /></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack.Screen</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"new-hike\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> options</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ presentation: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'modal'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, title: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Log a hike'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> /></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<p>Opening it is still <code dir=\"ltr\">router.push('/new-hike')</code> (or a <code dir=\"ltr\">Link</code>); closing it after a save is <code dir=\"ltr\">router.back()</code>, which pops the modal like any screen. Android has no card-style sheet by default, so the modal appears as a full-screen page with a different animation; give the form a visible Cancel button in <code dir=\"ltr\">headerLeft</code> so both platforms have an obvious way out.</p>\n<p>When the form has unsaved input, swiping it away loses the user’s typing. For long forms, confirm before discarding with an <code dir=\"ltr\">Alert</code> on Cancel; for a short form like Trailhead’s, a draft that’s quick to retype is an acceptable trade.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Protected routes</h2>\n<p>Trailhead syncs hikes to an account, so most screens require signing in. <code dir=\"ltr\">Stack.Protected</code> wraps routes in a condition:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">src/app/_layout.tsx</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { Stack } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'expo-router'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { useSession } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '../state/session'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> default</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> RootLayout</span><span style=\"color:#953800;--shiki-dark:#FFA657\">() </span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">isSignedIn</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">isLoading</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useSession</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (isLoading) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// the splash screen is still showing</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack.Protected</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> guard</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">isSignedIn</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack.Screen</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"sign-in\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> options</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ headerShown: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> /></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack.Protected</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack.Protected</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> guard</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">isSignedIn</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack.Screen</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"(tabs)\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> options</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ headerShown: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> /></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack.Screen</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"hikes/[id]\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> options</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ title: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Hike'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> /></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack.Screen</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"new-hike\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> options</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ presentation: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'modal'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, title: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Log a hike'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> /></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack.Protected</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  );</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>When <code dir=\"ltr\">guard</code> is false, those routes are unavailable: navigating to them, including from a deep link, sends the user to the first available screen instead, and any history entries for them are removed. Signing in flips <code dir=\"ltr\">isSignedIn</code>, the layout re-renders, and the protected screens appear without you calling <code dir=\"ltr\">router</code> at all. Signing out does the reverse, which also clears the back stack, so the back gesture can’t reveal a signed-in screen.</p>\n<p><code dir=\"ltr\">useSession</code> is a context like the one you built two lessons ago. Its <code dir=\"ltr\">isLoading</code> covers the moment at startup when Trailhead is still reading the saved token from secure storage (section 4); rendering nothing while the splash screen is up avoids a flash of the sign-in page for users who are already signed in.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Guarding inside each screen</span></p>\n<p>Checking <code dir=\"ltr\">if (!user) return &lt;Redirect href=&quot;/sign-in&quot; /&gt;</code> at the top of every screen works until someone adds a screen and forgets the check. A guard in the layout protects every route listed inside it, including ones added next year. Keep per-screen redirects for special cases.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">Deep links</h2>\n<p>A <strong>deep link</strong> opens the app on a specific screen. Give Trailhead a URL scheme in <code dir=\"ltr\">app.json</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"json\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">app.json</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"expo\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: {</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    \"name\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Trailhead\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    \"scheme\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"trailhead\"</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Now <code dir=\"ltr\">trailhead://hikes/42</code> opens the app and renders <code dir=\"ltr\">/hikes/42</code> with the same Stack you built, including a back button to the log. You get this for free from file-based routing: every route already has a URL. Schemes are part of the native app configuration, so test them in a development build; Expo Go has its own scheme. From a terminal:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">npx</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> uri-scheme</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> open</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> trailhead://hikes/42</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --ios</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">npx</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> uri-scheme</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> open</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> trailhead://hikes/42</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --android</span></span></code></pre></div>\n<p>Custom schemes have a weakness: any app can claim <code dir=\"ltr\">trailhead://</code>. For links you share on the web, use <strong>universal links</strong> (iOS) and <strong>App Links</strong> (Android), which use your own <code dir=\"ltr\">https://</code> domain and require hosting a small verification file on it. Expo’s linking guide walks through both.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Deep links are untrusted input</span></p>\n<p>A deep link can come from anywhere, with any id. The <code dir=\"ltr\">parseHikeParams</code> function from earlier in this section is exactly what those screens need: validate, then show “hike not found” for anything that doesn’t exist.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">How a URL picks a route</h2>\n<p>When a link arrives, Expo Router compares its path with every route file. Knowing the rules removes the guesswork:</p>\n<div class=\"table-wrap\" role=\"region\" aria-label=\"Scrollable table\" tabindex=\"0\"><table><thead>\n<tr>\n<th>Rule</th>\n<th>Example</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Groups like <code dir=\"ltr\">(tabs)</code> add no segment</td>\n<td><code dir=\"ltr\">(tabs)/stats.tsx</code> matches <code dir=\"ltr\">/stats</code></td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">index</code> adds no segment</td>\n<td><code dir=\"ltr\">settings/index.tsx</code> matches <code dir=\"ltr\">/settings</code></td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">[name]</code> matches exactly one segment</td>\n<td><code dir=\"ltr\">hikes/[id].tsx</code> matches <code dir=\"ltr\">/hikes/42</code></td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">[...name]</code> matches one or more remaining segments, as an array</td>\n<td><code dir=\"ltr\">trails/[...path].tsx</code> matches <code dir=\"ltr\">/trails/alps/mont-blanc</code></td>\n</tr>\n<tr>\n<td>Static beats dynamic beats catch-all</td>\n<td><code dir=\"ltr\">/hikes/new</code> picks <code dir=\"ltr\">hikes/new.tsx</code> over <code dir=\"ltr\">hikes/[id].tsx</code></td>\n</tr>\n<tr>\n<td>The query string becomes extra params</td>\n<td><code dir=\"ltr\">/hikes/42?units=mi</code> adds <code dir=\"ltr\">units</code></td>\n</tr>\n</tbody>\n</table></div><p>That ranking is why you can safely add <code dir=\"ltr\">hikes/new.tsx</code> next to <code dir=\"ltr\">hikes/[id].tsx</code>. In the exercise you’ll implement this matcher yourself, which is the fastest way to never be surprised by it again.</p>\n<p>That completes navigation. Section 4 connects Trailhead to the outside world: a server, the device’s storage, and its sensors.</p>\n","toc":[{"id":"sec-1","text":"The add-hike form as a modal","level":2},{"id":"sec-2","text":"Protected routes","level":2},{"id":"sec-3","text":"Deep links","level":2},{"id":"sec-4","text":"How a URL picks a route","level":2}],"takeaways":["A Stack screen with <code dir=\"ltr\">presentation: 'modal'</code> slides up as a sheet on iOS; dismiss it with <code dir=\"ltr\">router.back()</code> after saving.","<code dir=\"ltr\">Stack.Protected</code> with a <code dir=\"ltr\">guard</code> hides routes when the condition is false, so signed-out users can’t reach them, even from a deep link.","A <code dir=\"ltr\">scheme</code> in app.json makes <code dir=\"ltr\">trailhead://hikes/42</code> open the <code dir=\"ltr\">/hikes/[id]</code> route; custom schemes need a development build, not Expo Go.","When matching a URL, groups and <code dir=\"ltr\">index</code> add no segment, static segments beat dynamic ones, and <code dir=\"ltr\">[...rest]</code> catches whatever is left."],"further":[{"title":"Modals","url":"https://docs.expo.dev/router/advanced/modals/"},{"title":"Protected routes","url":"https://docs.expo.dev/router/advanced/protected/"},{"title":"Linking into your app","url":"https://docs.expo.dev/linking/into-your-app/"}],"quiz":[{"q":"<p>Trailhead has both <code dir=\"ltr\">src/app/hikes/new.tsx</code> and <code dir=\"ltr\">src/app/hikes/[id].tsx</code>. Which one renders for <code dir=\"ltr\">/hikes/new</code>?</p>","options":[{"text":"<code dir=\"ltr\">[id].tsx</code>, with <code dir=\"ltr\">id</code> set to “new”, because dynamic routes match anything.","why":"Dynamic routes do match any segment, but a static segment that matches exactly is preferred."},{"text":"Whichever file was created first.","why":"Matching doesn’t depend on file age. It ranks routes by how specific they are."},{"text":"Neither; Expo Router reports a conflict at startup.","why":"Static and dynamic siblings are a normal, supported setup."},{"text":"<code dir=\"ltr\">new.tsx</code>, because a static segment beats a dynamic one.","why":"Correct. <code dir=\"ltr\">/hikes/new</code> matches the static route; <code dir=\"ltr\">/hikes/42</code> falls through to <code dir=\"ltr\">[id].tsx</code>."}],"answer":3},{"q":"<p>Signed-out users tap an old email link to <code dir=\"ltr\">trailhead://hikes/42</code>. The hike route sits inside <code dir=\"ltr\">&lt;Stack.Protected guard={isSignedIn}&gt;</code>. What happens?</p>","options":[{"text":"The hike screen opens, then immediately shows an error.","why":"Protected routes aren’t rendered at all while the guard is false, so the hike screen never mounts."},{"text":"The user lands on the first available screen (Trailhead’s sign-in) instead of the protected route.","why":"Correct. While the guard is false the route is unavailable, and navigation to it is redirected."},{"text":"The app crashes because the route doesn’t exist.","why":"Protected routes are hidden, not deleted; Expo Router handles the attempt gracefully."}],"answer":1},{"q":"<p>You add a <code dir=\"ltr\">scheme</code> of “trailhead” to app.json and test <code dir=\"ltr\">trailhead://hikes/42</code> in Expo Go. Nothing opens. Why?</p>","options":[{"text":"Custom schemes are registered in the app’s native config, so they work in a development or production build of Trailhead, not inside Expo Go.","why":"Correct. Expo Go is a different app with its own scheme; build Trailhead itself to test its links."},{"text":"Schemes must start with <code dir=\"ltr\">https</code>.","why":"Universal links and App Links use https, but custom schemes like <code dir=\"ltr\">trailhead://</code> are valid too."},{"text":"Deep links only work when the app is already open.","why":"A deep link can cold-start the app and open the matching route directly."}],"answer":0}],"exercise":{"kind":"web","mode":"js","starter":{"js":"const ROUTES = [\n  '(tabs)/index',\n  '(tabs)/stats',\n  'settings/index',\n  'settings/units',\n  'hikes/new',\n  'hikes/[id]',\n  'hikes/[id]/photos',\n  'trails/[...path]',\n];\n\n// Return { route, params } for the best matching route file, or null.\nfunction matchRoute(url, routes) {\n  const path = url.replace(/^\\//, '');\n  const route = routes.find((r) => r === path);\n  return route ? { route, params: {} } : null;\n}\n\nconsole.log(JSON.stringify(matchRoute('/hikes/42?units=mi', ROUTES)));\n"},"solution":{"js":"const ROUTES = [\n  '(tabs)/index',\n  '(tabs)/stats',\n  'settings/index',\n  'settings/units',\n  'hikes/new',\n  'hikes/[id]',\n  'hikes/[id]/photos',\n  'trails/[...path]',\n];\n\nfunction segmentsOf(route) {\n  const segs = route.split('/').filter((s) => !/^\\(.+\\)$/.test(s));\n  if (segs[segs.length - 1] === 'index') segs.pop();\n  return segs;\n}\n\nfunction tryMatch(route, parts) {\n  const pattern = segmentsOf(route);\n  const params = {};\n  let rank = '';\n  for (let i = 0; i < pattern.length; i++) {\n    const seg = pattern[i];\n    const catchAll = seg.match(/^\\[\\.\\.\\.(\\w+)\\]$/);\n    const dynamic = seg.match(/^\\[(\\w+)\\]$/);\n    if (catchAll) {\n      const rest = parts.slice(i);\n      if (rest.length === 0) return null;\n      params[catchAll[1]] = rest;\n      return { route, params, rank: rank + '1' };\n    }\n    if (i >= parts.length) return null;\n    if (dynamic) {\n      params[dynamic[1]] = parts[i];\n      rank += '2';\n    } else if (seg === parts[i]) {\n      rank += '3';\n    } else {\n      return null;\n    }\n  }\n  return pattern.length === parts.length ? { route, params, rank } : null;\n}\n\nfunction matchRoute(url, routes) {\n  const [path, query = ''] = url.split('?');\n  const parts = path.split('/').filter(Boolean);\n  let best = null;\n  for (const route of routes) {\n    const m = tryMatch(route, parts);\n    if (m && (!best || m.rank > best.rank)) best = m;\n  }\n  if (!best) return null;\n  const params = { ...best.params };\n  for (const [key, value] of new URLSearchParams(query)) params[key] = value;\n  return { route: best.route, params };\n}\n\nconsole.log(JSON.stringify(matchRoute('/hikes/42?units=mi', ROUTES)));\n"},"checks":[{"id":"groups-index","test":"const R = ['(tabs)/index', '(tabs)/stats', 'settings/index', 'settings/units', 'hikes/new', 'hikes/[id]', 'hikes/[id]/photos', 'trails/[...path]']; const m = (u) => matchRoute(u, R); return typeof matchRoute === 'function' && m('/')?.route === '(tabs)/index' && m('/stats')?.route === '(tabs)/stats' && m('/settings')?.route === 'settings/index' && m('/settings/units')?.route === 'settings/units';"},{"id":"dynamic","test":"const R = ['(tabs)/index', '(tabs)/stats', 'settings/index', 'settings/units', 'hikes/new', 'hikes/[id]', 'hikes/[id]/photos', 'trails/[...path]']; const m = (u) => matchRoute(u, R); const a = m('/hikes/42'); const b = m('/hikes/42/photos'); return a?.route === 'hikes/[id]' && a.params.id === '42' && b?.route === 'hikes/[id]/photos' && b.params.id === '42';"},{"id":"static-wins","test":"const R = ['(tabs)/index', '(tabs)/stats', 'settings/index', 'settings/units', 'hikes/new', 'hikes/[id]', 'hikes/[id]/photos', 'trails/[...path]']; const m = (u) => matchRoute(u, R); return m('/hikes/new')?.route === 'hikes/new';"},{"id":"catch-all","test":"const R = ['(tabs)/index', '(tabs)/stats', 'settings/index', 'settings/units', 'hikes/new', 'hikes/[id]', 'hikes/[id]/photos', 'trails/[...path]']; const m = (u) => matchRoute(u, R); const a = m('/trails/alps/mont-blanc'); return a?.route === 'trails/[...path]' && Array.isArray(a.params.path) && a.params.path.join('/') === 'alps/mont-blanc' && m('/trails') === null;"},{"id":"query-slash","test":"const R = ['(tabs)/index', '(tabs)/stats', 'settings/index', 'settings/units', 'hikes/new', 'hikes/[id]', 'hikes/[id]/photos', 'trails/[...path]']; const m = (u) => matchRoute(u, R); const a = m('/hikes/7?units=mi'); return a?.route === 'hikes/[id]' && a.params.id === '7' && a.params.units === 'mi' && m('/stats/')?.route === '(tabs)/stats';"},{"id":"no-match","test":"const R = ['(tabs)/index', '(tabs)/stats', 'settings/index', 'settings/units', 'hikes/new', 'hikes/[id]', 'hikes/[id]/photos', 'trails/[...path]']; const m = (u) => matchRoute(u, R); return m('/nope') === null && m('/settings/units/extra') === null;"}],"title":"Match a deep link to a route file","prompt":"<p>Implement <code dir=\"ltr\">matchRoute(url, routes)</code> the way Expo Router picks a screen for a deep link. <code dir=\"ltr\">routes</code> are file paths without <code dir=\"ltr\">.tsx</code> (see <code dir=\"ltr\">ROUTES</code>). Return <code dir=\"ltr\">{ route, params }</code> for the best match, or <code dir=\"ltr\">null</code>.</p>\n<ul>\n<li>Groups such as <code dir=\"ltr\">(tabs)</code> and a final <code dir=\"ltr\">index</code> add no URL segment.</li>\n<li><code dir=\"ltr\">[id]</code> matches exactly one segment and adds it to <code dir=\"ltr\">params</code> as a string.</li>\n<li><code dir=\"ltr\">[...path]</code> matches <strong>one or more</strong> remaining segments and adds them as an array.</li>\n<li>When several routes match, prefer static segments over <code dir=\"ltr\">[dynamic]</code> ones, and <code dir=\"ltr\">[dynamic]</code> over <code dir=\"ltr\">[...catchAll]</code>, comparing from the first segment.</li>\n<li>Ignore a trailing slash; add query-string values (<code dir=\"ltr\">?units=mi</code>) to <code dir=\"ltr\">params</code>.</li>\n</ul>\n","hints":["Turn each route into its URL segments first: drop <code dir=\"ltr\">(group)</code> segments and a trailing <code dir=\"ltr\">index</code>. Split the URL path with <code dir=\"ltr\">.split('/').filter(Boolean)</code>.","Walk the route’s segments alongside the URL’s. A literal must equal the URL segment; <code dir=\"ltr\">[x]</code> captures it; <code dir=\"ltr\">[...x]</code> captures <code dir=\"ltr\">parts.slice(i)</code> and ends the match. Afterwards, the lengths must line up.","To rank, build a string per match with one digit per segment (3 static, 2 dynamic, 1 catch-all) and keep the match with the largest string. <code dir=\"ltr\">new URLSearchParams(query)</code> iterates the query pairs."],"explanation":"<p>The solution normalizes each route file into URL segments, tries to match it segment by segment while collecting params, and scores each segment so that more specific routes win: comparing the score strings ranks static before dynamic before catch-all from left to right. That’s why <code dir=\"ltr\">/hikes/new</code> reaches <code dir=\"ltr\">hikes/new.tsx</code> while <code dir=\"ltr\">/hikes/42</code> falls through to <code dir=\"ltr\">hikes/[id].tsx</code>, and why the screen must still validate <code dir=\"ltr\">id</code>: the matcher accepts any segment at all.</p>\n","checkLabels":{"groups-index":"Groups and index files add no URL segment","dynamic":"Dynamic segments match and become string params","static-wins":"A static route beats a dynamic one","catch-all":"Catch-all routes collect the remaining segments as an array","query-slash":"Query strings become params and trailing slashes are ignored","no-match":"Unknown or too-long paths return null"}},"words":724,"readMinutes":3}