{"id":"l-rn-3-2","courseId":"react-native-apps","lang":"en","kind":"lesson","summary":"Give Trailhead bottom tabs with a route group, add a hike detail screen with a dynamic [id] route, and read its params safely, remembering that params always arrive as strings.","html":"<p>Trailhead now has more than one place to be: the hike log, a stats page, and settings, plus a detail screen for each hike. The first three are peers that users switch between all the time; the detail screen is something you drill into and come back from. That’s the classic mobile shape: <strong>tabs</strong> for peers, a <strong>stack</strong> for drilling in.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">A route group for the tabs</h2>\n<p>Here’s the file structure Trailhead uses:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"text\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Text</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>src/app/\n  _layout.tsx          root Stack\n  (tabs)/\n    _layout.tsx        Tabs navigator\n    index.tsx          /          (Log)\n    stats.tsx          /stats\n    settings.tsx       /settings\n  hikes/\n    [id].tsx           /hikes/42  (pushed over the tabs)</code></pre></div>\n<p>A folder in parentheses is a <strong>route group</strong>. It lets the three tab screens share a layout without adding anything to their URLs: <code dir=\"ltr\">(tabs)/stats.tsx</code> is <code dir=\"ltr\">/stats</code>, not <code dir=\"ltr\">/tabs/stats</code>.</p>\n<p>The tab layout returns a <code dir=\"ltr\">Tabs</code> navigator, one <code dir=\"ltr\">Tabs.Screen</code> per tab, in the order they should appear:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">src/app/(tabs)/_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\"> Ionicons </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '@expo/vector-icons/Ionicons'</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\"> { Tabs } </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>\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\"> TabLayout</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\">  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\">Tabs</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> screenOptions</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ tabBarActiveTintColor: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'#2f6f4e'</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\">Tabs.Screen</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">        name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"index\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">        options</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\">          title: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Log'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">          tabBarIcon</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: ({ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">color</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">size</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Ionicons</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\">\"list\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> color</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">color</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> size</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">size</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\">        }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</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\">      &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Tabs.Screen</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">        name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"stats\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">        options</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\">          title: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Stats'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">          tabBarIcon</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: ({ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">color</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">size</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Ionicons</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\">\"stats-chart\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> color</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">color</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> size</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">size</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\">        }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</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\">      &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Tabs.Screen</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">        name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"settings\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">        options</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\">          title: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Settings'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">          tabBarIcon</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: ({ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">color</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">size</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Ionicons</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\">\"settings-outline\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> color</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">color</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> size</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">size</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\">        }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</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\">    &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Tabs</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>The root layout then puts the whole tab group into the Stack, with the group’s own header hidden (each tab has its own), followed by the detail route:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"2\"><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>\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\">  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.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</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>Because <code dir=\"ltr\">hikes/[id]</code> belongs to the root Stack rather than to the tabs, opening a hike slides a full screen over the tab bar, and the back gesture returns to whichever tab you came from. That’s what most iOS and Android apps do, and it gives the detail screen the whole height of the phone.</p>\n<p>Two habits keep tabs pleasant. Keep them to between three and five top-level destinations users visit often; a sixth tab usually means some of them belong inside settings. And remember that tabs stay mounted once visited: switch from Log to Stats and back, and the log keeps its scroll position. That’s the behaviour users expect, so don’t reset it. If one tab needs its own drill-down screens that keep the tab bar visible, give that tab a folder with its own <code dir=\"ltr\">_layout.tsx</code> returning a <code dir=\"ltr\">Stack</code>; nesting navigators is just nesting folders.</p>\n<aside class=\"callout callout-note\" 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\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4\"/><path d=\"M12 8h.01\"/></svg><span><span class=\"callout-kind\">Note</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Native tabs</span></p>\n<p>This <code dir=\"ltr\">Tabs</code> component draws its tab bar in JavaScript, so you can style it freely. Expo Router also offers native tabs that use the system’s own tab bar, including its newest visual styles, with less room for custom styling. The import path has changed between SDK versions, so follow the Expo Router docs for your SDK if you choose them.</p>\n</aside>\n<h2 id=\"sec-2\" tabindex=\"-1\">Dynamic routes and params</h2>\n<p>A file named with square brackets, <code dir=\"ltr\">[id].tsx</code>, matches any single URL segment and exposes it as a param named <code dir=\"ltr\">id</code>. Read it with <code dir=\"ltr\">useLocalSearchParams</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">src/app/hikes/[id].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, useLocalSearchParams } </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\"> { Text, View } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'react-native'</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\"> { useHike } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '../../state/hikes'</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\"> HikeDetail</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\">id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useLocalSearchParams</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;{ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</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:#0550AE;--shiki-dark:#79C0FF\"> hike</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useHike</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(id);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">hike) {</span></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\">View</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> style</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ flex: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, padding: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">24</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\">Text</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>That hike doesn't exist anymore.&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Text</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\">View</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>\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\">View</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> style</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ flex: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, padding: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">16</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\"> options</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ title: hike.name }</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\">Text</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> style</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ fontSize: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">16</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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">hike.distanceKm</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> km&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Text</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\">View</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>To get there, link to the URL. Either build the string or let Expo Router fill in the pattern:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">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\">Link</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> href</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`/hikes/${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">hike</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">id</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">hike.name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Link</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\">Link</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> href</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ pathname: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'/hikes/[id]'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, params: { id: hike.id } }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">hike.name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Link</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<p>Anything in <code dir=\"ltr\">params</code> that isn’t part of the path becomes a query string: <code dir=\"ltr\">params: { id: hike.id, units: 'mi' }</code> produces <code dir=\"ltr\">/hikes/42?units=mi</code>, and the detail screen reads <code dir=\"ltr\">units</code> with the same hook.</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>Trusting the param’s type</span></p>\n<p><code dir=\"ltr\">useLocalSearchParams&lt;{ id: string }&gt;()</code> looks typed, but the generic is a promise you make to TypeScript, not a conversion. Params come from a URL, so they are <strong>strings</strong>, and a repeated query key (<code dir=\"ltr\">?tag=lake&amp;tag=steep</code>) arrives as an <strong>array</strong> of strings. A deep link from an old email can also contain anything at all: <code dir=\"ltr\">/hikes/abc</code>, <code dir=\"ltr\">/hikes/-1</code>. Convert, validate and handle the “not found” case on every screen with params.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">Ids in params, data from state</h2>\n<p>Pass the smallest thing that identifies the data, usually an id, and look the data up on the destination screen. Passing a whole hike object seems convenient, but params are serialized into the URL: the object becomes a string, a deep link can never recreate it, and if the user edits the hike, the detail screen shows the stale copy it was handed. The next lesson builds the shared state that <code dir=\"ltr\">useHike(id)</code> reads from.</p>\n<p><code dir=\"ltr\">useLocalSearchParams</code> returns the params of the screen it’s called in, which is what you want almost always. Its sibling <code dir=\"ltr\">useGlobalSearchParams</code> updates whenever any route changes, even routes in the background, which causes extra renders; reserve it for things like analytics that genuinely care about every navigation.</p>\n<p>Next, the state behind those lookups: one source of truth for hikes, shared by every tab and screen.</p>\n","toc":[{"id":"sec-1","text":"A route group for the tabs","level":2},{"id":"sec-2","text":"Dynamic routes and params","level":2},{"id":"sec-3","text":"Ids in params, data from state","level":2}],"takeaways":["A folder in parentheses, like <code dir=\"ltr\">(tabs)</code>, groups routes under a layout without adding a segment to the URL.","Put the tab navigator in <code dir=\"ltr\">(tabs)/_layout.tsx</code> and push detail screens from the root Stack so they cover the tab bar.","A file named <code dir=\"ltr\">[id].tsx</code> matches any single segment; read it with <code dir=\"ltr\">useLocalSearchParams()</code>.","Route params are strings (or arrays of strings), so convert and validate them before use.","Pass ids in params, never whole objects, and look the data up on the destination screen."],"further":[{"title":"Navigation layouts","url":"https://docs.expo.dev/router/basics/navigation-layouts/"},{"title":"Tabs","url":"https://docs.expo.dev/router/advanced/tabs/"},{"title":"URL parameters","url":"https://docs.expo.dev/router/reference/url-parameters/"}],"quiz":[{"q":"<p>What URL does <code dir=\"ltr\">src/app/(tabs)/stats.tsx</code> serve?</p>","options":[{"text":"<code dir=\"ltr\">/(tabs)/stats</code>","why":"Parentheses mark a group, and groups never appear in the URL."},{"text":"<code dir=\"ltr\">/tabs/stats</code>","why":"The group name is dropped entirely, not just the parentheses."},{"text":"<code dir=\"ltr\">/stats</code>","why":"Correct. The <code dir=\"ltr\">(tabs)</code> group only decides which layout wraps the screen."}],"answer":2},{"q":"<p>The detail screen does this, and a hike with id 7 never matches. Why?</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">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\">const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useLocalSearchParams</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;{ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</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:#0550AE;--shiki-dark:#79C0FF\"> hike</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> hikes.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">find</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">h</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> h.id </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> id); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// h.id is a number</span></span></code></pre></div>\n","options":[{"text":"<code dir=\"ltr\">useLocalSearchParams</code> only works inside tab screens.","why":"It works in any route. The issue is the comparison, not where the hook runs."},{"text":"<code dir=\"ltr\">id</code> is the string “7”, and <code dir=\"ltr\">7 === &quot;7&quot;</code> is false; convert with <code dir=\"ltr\">Number(id)</code> and check the result.","why":"Correct. URL params are always strings. The generic only types them; it doesn’t convert anything."},{"text":"The route file must be called <code dir=\"ltr\">[hikeId].tsx</code> for the param to arrive.","why":"The file name only sets the param’s name. <code dir=\"ltr\">[id].tsx</code> gives you <code dir=\"ltr\">id</code>."}],"answer":1},{"q":"<p>A teammate passes a whole hike object as a route param so the detail screen doesn’t need to look it up. What’s the main problem?</p>","options":[{"text":"Params are serialized into the URL, so the object gets stringified, deep links can’t recreate it, and the screen shows stale data after an edit.","why":"Correct. Pass the id and read the current hike from your state or storage on the destination screen."},{"text":"Route params are limited to one character.","why":"There’s no such limit. The problem is that URLs carry strings, not objects."},{"text":"Objects in params make the app crash on Android only.","why":"It misbehaves the same way on both platforms. It just doesn’t crash."},{"text":"Expo Router encrypts params, so it would be slow.","why":"Params aren’t encrypted; they’re part of a URL. Speed isn’t the issue."}],"answer":0}],"exercise":{"kind":"web","mode":"js","starter":{"js":"// params is whatever useLocalSearchParams() returned for /hikes/[id]\n// Return { id, units, photo } with real types and safe defaults.\nfunction parseHikeParams(params) {\n  return { id: params.id, units: params.units, photo: params.photo };\n}\n\nconsole.log(parseHikeParams({ id: '42', units: 'MI', photo: '2' }));\nconsole.log(parseHikeParams({ id: 'abc' }));\n"},"solution":{"js":"function first(value) {\n  return Array.isArray(value) ? value[0] : value;\n}\n\nfunction parseHikeParams(params) {\n  const rawId = first(params.id);\n  const id = /^\\d+$/.test(rawId ?? '') && Number(rawId) > 0 ? Number(rawId) : null;\n\n  const rawUnits = String(first(params.units) ?? '').toLowerCase();\n  const units = rawUnits === 'mi' ? 'mi' : 'km';\n\n  const rawPhoto = first(params.photo);\n  const photo = /^\\d+$/.test(rawPhoto ?? '') ? Number(rawPhoto) : 0;\n\n  return { id, units, photo };\n}\n\nconsole.log(parseHikeParams({ id: '42', units: 'MI', photo: '2' }));\nconsole.log(parseHikeParams({ id: 'abc' }));\n"},"checks":[{"id":"valid","test":"return typeof parseHikeParams === 'function' && (() => { const r = parseHikeParams({ id: '42', units: 'mi', photo: '2' }); return r.id === 42 && r.units === 'mi' && r.photo === 2; })();"},{"id":"bad-id","test":"return (() => { const r = parseHikeParams({ id: 'abc' }); return r.id === null && r.units === 'km' && r.photo === 0; })() && parseHikeParams({ id: '0' }).id === null && parseHikeParams({ id: '4.5' }).id === null && parseHikeParams({ id: '-3' }).id === null && parseHikeParams({}).id === null;"},{"id":"units","test":"return parseHikeParams({ id: '1', units: 'MI' }).units === 'mi' && parseHikeParams({ id: '1', units: 'miles' }).units === 'km' && parseHikeParams({ id: '1' }).units === 'km';"},{"id":"photo","test":"return parseHikeParams({ id: '1', photo: '0' }).photo === 0 && parseHikeParams({ id: '1', photo: '3' }).photo === 3 && parseHikeParams({ id: '1', photo: 'x' }).photo === 0 && parseHikeParams({ id: '1', photo: '-2' }).photo === 0;"},{"id":"arrays","test":"const r = parseHikeParams({ id: ['7', '8'], units: ['mi', 'km'], photo: ['1'] }); return r.id === 7 && r.units === 'mi' && r.photo === 1;"}],"title":"Read route params safely","prompt":"<p>Trailhead’s detail screen at <code dir=\"ltr\">/hikes/[id]</code> can be opened from the app, from a notification, or from an old link with anything in it. Write <code dir=\"ltr\">parseHikeParams(params)</code> that takes the object from <code dir=\"ltr\">useLocalSearchParams()</code> and returns <code dir=\"ltr\">{ id, units, photo }</code>:</p>\n<ul>\n<li><code dir=\"ltr\">id</code>: a <strong>positive whole number</strong>, or <code dir=\"ltr\">null</code> if missing or invalid (<code dir=\"ltr\">'abc'</code>, <code dir=\"ltr\">'0'</code>, <code dir=\"ltr\">'4.5'</code>, <code dir=\"ltr\">'-3'</code>).</li>\n<li><code dir=\"ltr\">units</code>: <code dir=\"ltr\">'mi'</code> if the param is <code dir=\"ltr\">mi</code> in any case; otherwise <code dir=\"ltr\">'km'</code>.</li>\n<li><code dir=\"ltr\">photo</code>: a whole number of 0 or more (which photo to show first); <code dir=\"ltr\">0</code> if missing or invalid.</li>\n</ul>\n<p>Any param may also arrive as an <strong>array</strong> of strings (a repeated query key). Use the first value.</p>\n","hints":["Write a tiny helper first: <code dir=\"ltr\">const first = (v) =&gt; Array.isArray(v) ? v[0] : v;</code>.","<code dir=\"ltr\">/^\\d+$/</code> accepts only digits, so it rejects <code dir=\"ltr\">-3</code>, <code dir=\"ltr\">4.5</code> and <code dir=\"ltr\">abc</code> in one test. Then check the number is above 0 for the id.","For units, lower-case the string and compare with <code dir=\"ltr\">'mi'</code>; everything else falls back to <code dir=\"ltr\">'km'</code>."],"explanation":"<p>Params come from a URL, so they’re strings or arrays of strings, never numbers, whatever the TypeScript generic says. Normalizing them in one pure function gives the screen real types and safe defaults, and turns a broken deep link into <code dir=\"ltr\">id: null</code>, which the screen renders as a friendly “hike not found” instead of a crash. The digits-only regular expression is stricter than <code dir=\"ltr\">Number()</code>, which happily accepts <code dir=\"ltr\">'4.5'</code>, <code dir=\"ltr\">' 7 '</code> and <code dir=\"ltr\">''</code> (as 0).</p>\n","checkLabels":{"valid":"A normal set of params is converted to real types","bad-id":"Invalid or missing ids become null","units":"Units are case-insensitive and default to km","photo":"Photo index defaults to 0 for missing or invalid values","arrays":"Array params use their first value"}},"words":659,"readMinutes":3}