{"id":"l-rn-4-1","courseId":"react-native-apps","lang":"en","kind":"lesson","summary":"Load Trailhead’s trail catalog from a server with fetch, model loading, error, empty and success states explicitly, cancel stale requests, and handle the flaky networks phones live on.","html":"<p>Up to now, Trailhead’s data has lived on the phone. The next feature, a catalog of trails near you, comes from a server. On a laptop with office Wi-Fi, a request is a formality. On a phone at a trailhead with one bar of signal, it’s the slowest and least reliable thing your app does: requests take seconds, fail halfway, or never come back. Designing for that is most of the work.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Four states, one value</h2>\n<p>Every screen that loads data has at least four states the user can see: <strong>loading</strong>, <strong>error</strong>, <strong>empty</strong> (it loaded, and there’s nothing), and <strong>success</strong>. Model them as one value, so impossible combinations can’t happen:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">src/lib/types.ts</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\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Trail</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </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 style=\"color:#953800;--shiki-dark:#FFA657\">name</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 style=\"color:#953800;--shiki-dark:#FFA657\">lengthKm</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">difficulty</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'easy'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'moderate'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'hard'</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\"> type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Load</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  |</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">status</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'loading'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  |</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">status</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'error'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">message</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\">  |</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">status</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'success'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">data</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span></code></pre></div>\n<p>Empty isn’t a separate status; it’s success with zero items, and the screen decides how to show it.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 660 230\" role=\"img\" aria-labelledby=\"l-rn-4-1-t1\"><defs><marker id=\"arrow-l-rn-4-1-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-rn-4-1-t1\">The screen starts in loading. A successful response moves it to success, which shows either the list or an empty state. A failure moves it to error, and Retry moves it back to loading. Pull to refresh goes from success back to loading.</title>\n  <rect class=\"d-box-accent\" x=\"250\" y=\"20\" width=\"160\" height=\"54\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"330\" y=\"53\" text-anchor=\"middle\">loading</text>\n  <rect class=\"d-box-success\" x=\"440\" y=\"150\" width=\"190\" height=\"60\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"535\" y=\"176\" text-anchor=\"middle\">success</text>\n  <text class=\"d-label-muted\" x=\"535\" y=\"198\" text-anchor=\"middle\">list or empty state</text>\n  <rect class=\"d-box-warn\" x=\"30\" y=\"150\" width=\"190\" height=\"60\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"125\" y=\"176\" text-anchor=\"middle\">error</text>\n  <text class=\"d-label-muted\" x=\"125\" y=\"198\" text-anchor=\"middle\">message + Retry</text>\n  <path class=\"d-arrow\" d=\"M380 74 L500 150\" marker-end=\"url(#arrow-l-rn-4-1-1)\"/>\n  <text class=\"d-label-muted\" x=\"480\" y=\"104\" text-anchor=\"middle\">response ok</text>\n  <path class=\"d-arrow\" d=\"M280 74 L160 150\" marker-end=\"url(#arrow-l-rn-4-1-1)\"/>\n  <text class=\"d-label-muted\" x=\"180\" y=\"104\" text-anchor=\"middle\">fails</text>\n  <path class=\"d-arrow d-dashed\" d=\"M220 168 C300 140 280 110 300 76\" marker-end=\"url(#arrow-l-rn-4-1-1)\"/>\n  <text class=\"d-label-muted\" x=\"300\" y=\"150\" text-anchor=\"middle\">Retry</text>\n  <path class=\"d-arrow d-dashed\" d=\"M440 168 C380 140 380 110 362 76\" marker-end=\"url(#arrow-l-rn-4-1-1)\"/>\n  <text class=\"d-label-muted\" x=\"400\" y=\"150\" text-anchor=\"middle\">refresh</text>\n</svg><figcaption>A request moves between explicit states</figcaption></figure>\n<h2 id=\"sec-2\" tabindex=\"-1\">Fetching with cleanup</h2>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">src/app/(tabs)/explore.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\"> { useCallback, useEffect, useState } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'react'</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\"> { ActivityIndicator, FlatList, Pressable, 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:#CF222E;--shiki-dark:#FF7B72\"> type</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { Load, Trail } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '../../lib/types'</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\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> API</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> process.env.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">EXPO_PUBLIC_API_URL</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\">async</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> getTrails</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">signal</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> AbortSignal</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Promise</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Trail</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\"> res</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> fetch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`${</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">API</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}/trails?near=current`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { signal });</span></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\">res.ok) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">throw</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Error</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`Server responded ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">res</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">status</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</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\"> res.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">json</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>\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\"> Explore</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\">state</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">setState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Load</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Trail</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[]>>({ status: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'loading'</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\">attempt</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">setAttempt</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</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\"> retry</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useCallback</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> setAttempt</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">n</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> n </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">), []);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  useEffect</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:#CF222E;--shiki-dark:#FF7B72\">    const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> controller</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> AbortController</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">    setState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ status: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'loading'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> });</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">    getTrails</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(controller.signal)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      .</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">then</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">data</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> setState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ status: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'success'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, data }))</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      .</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">catch</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:#CF222E;--shiki-dark:#FF7B72\">        if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (controller.signal.aborted) </span><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:#8250DF;--shiki-dark:#D2A8FF\">        setState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ status: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'error'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, message: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Could not load trails. Check your connection and try again.'</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:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> controller.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">abort</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }, [attempt]);</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\"> (state.status </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'loading'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">ActivityIndicator</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\"> }</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:#0A3069;--shiki-dark:#A5D6FF\">\"large\"</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\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (state.status </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'error'</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\">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\">, justifyContent: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'center'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, alignItems: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'center'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, gap: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">12</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:#0550AE;--shiki-dark:#79C0FF\"> style</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ textAlign: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'center'</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\">state.message</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\">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\">Pressable</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onPress</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">retry</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> accessibilityRole</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"button\"</span><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\">>Try again&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Text</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Pressable</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\">FlatList</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">      data</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">state.data</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">      keyExtractor</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">t</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> t.id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">      renderItem</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">item</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\">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\">{ 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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">item.name</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\">item.lengthKm</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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">      ListEmptyComponent</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\">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\">{ 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\">>No trails within 50 km. Try a wider search.&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Text</span><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\">  );</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Three details carry the weight. <code dir=\"ltr\">res.ok</code> is checked by hand, because <code dir=\"ltr\">fetch</code> treats a 500 as a successful delivery of bad news. The <code dir=\"ltr\">AbortController</code> cancels the request in the cleanup, so leaving the screen (or retrying) never lets a stale response overwrite a fresh one. And the error message tells the user what to do, rather than printing <code dir=\"ltr\">TypeError: Network request failed</code>.</p>\n<p><code dir=\"ltr\">EXPO_PUBLIC_API_URL</code> comes from a <code dir=\"ltr\">.env</code> file. Variables prefixed with <code dir=\"ltr\">EXPO_PUBLIC_</code> are inlined into the JavaScript bundle, which also means they’re <strong>readable by anyone</strong> who unpacks your app. Put URLs there, never secret keys.</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>Calling localhost from a phone</span></p>\n<p><code dir=\"ltr\">fetch('http://localhost:3000/trails')</code> works in a web browser on your laptop and fails on a phone, because there <code dir=\"ltr\">localhost</code> is the phone. Use your computer’s network address (<code dir=\"ltr\">http://192.168.1.20:3000</code>) during development; the Android Emulator reaches the host machine at <code dir=\"ltr\">10.0.2.2</code>. Both platforms also restrict plain <code dir=\"ltr\">http://</code> in release builds, so production APIs should be <code dir=\"ltr\">https://</code>.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">Pull to refresh and slow networks</h2>\n<p>Users expect to drag a list down to reload it. FlatList supports that with two props:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"2\"><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\">FlatList</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">  // …</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  refreshing</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">refreshing</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  onRefresh</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={async</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:#8250DF;--shiki-dark:#D2A8FF\">    setRefreshing</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">true</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    try</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ status: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'success'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, data: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> getTrails</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> AbortController</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">().signal) }); }</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    catch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/* keep the old list and show a short \"Couldn't refresh\" message */</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    finally</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setRefreshing</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">false</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></code></pre></div>\n<p>A refresh keeps the old list visible while it loads, which is friendlier than the full-screen spinner of the first load. If the refresh fails, keep the old data and show a short message, instead of replacing a perfectly good list with an error screen.</p>\n<p>Flaky networks also argue for <strong>automatic retries</strong> with a growing delay between attempts (1 s, 2 s, 4 s), so a brief signal drop recovers by itself. Only retry what can succeed later: a timeout or a 503 might, but a 404 or a 401 won’t, so failing fast is kinder. You’ll write that helper in the exercise.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Offline is a normal state, not an error</h2>\n<p>For a hiking app, no signal is the expected condition for hours at a time. Treat it that way. Show the last list you loaded, with a quiet banner saying when it was updated, instead of an error screen. Let users keep logging hikes, save them on the device, and send them when the connection returns. The community package <code dir=\"ltr\">@react-native-community/netinfo</code> tells you whether the device is connected and notifies you when that changes, which is the moment to retry queued work.</p>\n<p>None of that needs to be clever on day one. What matters is the decision: for each screen, what does the user see with no network? Write the answer down before writing the fetch, and design the empty, error and offline states with the same care as the happy path.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">When to use a data library</h2>\n<p>The code above is fine for one screen. Across a real app you’d repeat it everywhere, and you’d still lack caching (revisit a screen and see data instantly), request deduplication, background refetching and retries. <strong>TanStack Query</strong> handles all of that with <code dir=\"ltr\">useQuery</code>, and its docs include the small React Native setup that refetches when the app returns to the foreground. Learn the manual version first so you know what the library is doing, then use the library in production.</p>\n<p>Next: data that has to survive without any network at all, stored on the device.</p>\n","toc":[{"id":"sec-1","text":"Four states, one value","level":2},{"id":"sec-2","text":"Fetching with cleanup","level":2},{"id":"sec-3","text":"Pull to refresh and slow networks","level":2},{"id":"sec-4","text":"Offline is a normal state, not an error","level":2},{"id":"sec-5","text":"When to use a data library","level":2}],"takeaways":["Model a request as one status value (loading, error or success) instead of several booleans that can contradict each other.","<code dir=\"ltr\">fetch</code> only rejects on network failure; check <code dir=\"ltr\">response.ok</code> yourself for 404 and 500 responses.","Abort in-flight requests in the effect’s cleanup, because users leave screens before slow requests finish.","Every list that loads data needs four designed states, including an empty state and an error state with a retry button.","On a phone, <code dir=\"ltr\">localhost</code> is the phone itself; point development builds at your computer’s network address or a deployed API."],"further":[{"title":"Networking","url":"https://reactnative.dev/docs/network"},{"title":"Using fetch","url":"https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch"},{"title":"RefreshControl","url":"https://reactnative.dev/docs/refreshcontrol"}],"quiz":[{"q":"<p>Trailhead’s API returns <code dir=\"ltr\">404 Not Found</code> for a deleted trail. What does <code dir=\"ltr\">await fetch(url)</code> do?</p>","options":[{"text":"It throws, so the <code dir=\"ltr\">catch</code> block handles it.","why":"fetch only rejects when no response arrives at all (offline, DNS failure, aborted). A 404 is a response."},{"text":"It resolves with a response whose <code dir=\"ltr\">ok</code> is <code dir=\"ltr\">false</code> and <code dir=\"ltr\">status</code> is 404, which you must check.","why":"Correct. Check <code dir=\"ltr\">if (!res.ok)</code> and throw or handle it yourself."},{"text":"It resolves with <code dir=\"ltr\">undefined</code>.","why":"fetch always resolves with a Response object when the server answers, whatever the status code."}],"answer":1},{"q":"<p>A user opens a trail’s detail screen and immediately goes back. The request finishes two seconds later and calls <code dir=\"ltr\">setState</code>. What should the effect have done?</p>","options":[{"text":"Nothing; React Native ignores state updates from unmounted screens, so it’s harmless.","why":"The update is dropped, but the request still used data and battery, and a slow earlier response can overwrite a newer one when the id changes."},{"text":"Wrapped the call in <code dir=\"ltr\">setTimeout</code> so it runs after navigation.","why":"Delaying the request doesn’t stop it from finishing after the screen is gone."},{"text":"Created an <code dir=\"ltr\">AbortController</code> and called <code dir=\"ltr\">controller.abort()</code> in the cleanup function.","why":"Correct. Aborting cancels the request on unmount or when the id changes, so stale responses never land."}],"answer":2},{"q":"<p>Which state shape best fits Trailhead’s catalog screen?</p>","options":[{"text":"<code dir=\"ltr\">{ status: 'loading' } | { status: 'error'; error: string } | { status: 'success'; trails: Trail[] }</code>","why":"Correct. Only valid combinations exist, and TypeScript makes you handle each one."},{"text":"<code dir=\"ltr\">isLoading</code>, <code dir=\"ltr\">hasError</code> and <code dir=\"ltr\">trails</code> as three separate <code dir=\"ltr\">useState</code> calls.","why":"Nothing prevents <code dir=\"ltr\">isLoading</code> and <code dir=\"ltr\">hasError</code> being true together, or stale trails showing next to an error."},{"text":"<code dir=\"ltr\">trails: Trail[] | null</code>, where <code dir=\"ltr\">null</code> means loading or error.","why":"Loading and error look the same, so the screen can’t show the right message."},{"text":"A single <code dir=\"ltr\">trails</code> array that starts empty.","why":"An empty array can’t tell “still loading” from “no trails nearby”, which are very different screens."}],"answer":0}],"exercise":{"kind":"web","mode":"js","starter":{"js":"const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\n\n// Run task(attempt). Retry failures with a doubling delay; give up fast on errors\n// that can't succeed later. `sleep` is injectable so tests don't really wait.\nasync function withRetry(task, { retries = 2, baseDelayMs = 1000, sleep = wait } = {}) {\n  return task(1);\n}\n\n// Demo: fails twice with a 503, then succeeds.\nlet calls = 0;\nconst flaky = async () => {\n  calls += 1;\n  if (calls < 3) throw Object.assign(new Error('Service unavailable'), { status: 503 });\n  return ['Ridge Loop', 'Lake Trail'];\n};\nwithRetry(flaky, { baseDelayMs: 10 }).then((v) => console.log('got', v, 'after', calls, 'calls'), (e) => console.log('failed:', e.message));\n"},"solution":{"js":"const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\n\nfunction isRetryable(error) {\n  const status = error && error.status;\n  if (typeof status !== 'number') return true; // network failure, timeout\n  if (status === 408 || status === 429) return true;\n  return status < 400 || status >= 500;\n}\n\nasync function withRetry(task, { retries = 2, baseDelayMs = 1000, sleep = wait } = {}) {\n  for (let attempt = 1; ; attempt++) {\n    try {\n      return await task(attempt);\n    } catch (error) {\n      if (attempt > retries || !isRetryable(error)) throw error;\n      await sleep(baseDelayMs * 2 ** (attempt - 1));\n    }\n  }\n}\n\nlet calls = 0;\nconst flaky = async () => {\n  calls += 1;\n  if (calls < 3) throw Object.assign(new Error('Service unavailable'), { status: 503 });\n  return ['Ridge Loop', 'Lake Trail'];\n};\nwithRetry(flaky, { baseDelayMs: 10 }).then((v) => console.log('got', v, 'after', calls, 'calls'), (e) => console.log('failed:', e.message));\n"},"checks":[{"id":"returns-value","test":"const delays = []; const sleep = async (ms) => { delays.push(ms); }; const fail = (status) => Object.assign(new Error('x' + status), { status }); const v = await withRetry(async () => 'ok', { sleep }); return v === 'ok' && delays.length === 0;"},{"id":"retries-then-succeeds","test":"const delays = []; const sleep = async (ms) => { delays.push(ms); }; const fail = (status) => Object.assign(new Error('x' + status), { status }); let n = 0; const v = await withRetry(async (attempt) => { n++; if (attempt < 3) throw fail(503); return 'done'; }, { retries: 2, baseDelayMs: 100, sleep }); return v === 'done' && n === 3;"},{"id":"backoff","test":"const delays = []; const sleep = async (ms) => { delays.push(ms); }; const fail = (status) => Object.assign(new Error('x' + status), { status }); let n = 0; try { await withRetry(async () => { n++; throw new Error('offline'); }, { retries: 3, baseDelayMs: 100, sleep }); } catch {} return n === 4 && delays.join() === '100,200,400';"},{"id":"gives-up","test":"const delays = []; const sleep = async (ms) => { delays.push(ms); }; const fail = (status) => Object.assign(new Error('x' + status), { status }); let caught; try { await withRetry(async () => { throw fail(500); }, { retries: 1, baseDelayMs: 1, sleep }); } catch (e) { caught = e; } return caught instanceof Error && caught.status === 500;"},{"id":"no-retry-4xx","test":"const delays = []; const sleep = async (ms) => { delays.push(ms); }; const fail = (status) => Object.assign(new Error('x' + status), { status }); let n = 0; let caught; try { await withRetry(async () => { n++; throw fail(404); }, { retries: 3, sleep }); } catch (e) { caught = e; } return n === 1 && caught?.status === 404 && delays.length === 0;"},{"id":"retry-429","test":"const delays = []; const sleep = async (ms) => { delays.push(ms); }; const fail = (status) => Object.assign(new Error('x' + status), { status }); let n = 0; const v = await withRetry(async () => { n++; if (n === 1) throw fail(429); if (n === 2) throw fail(408); return 'ok'; }, { retries: 2, baseDelayMs: 50, sleep }); return v === 'ok' && n === 3 && delays.join() === '50,100';"}],"title":"Retry flaky requests with backoff","prompt":"<p>Implement <code dir=\"ltr\">withRetry(task, { retries, baseDelayMs, sleep })</code> for Trailhead’s API calls on bad mountain signal.</p>\n<ul>\n<li>Call <code dir=\"ltr\">task(attempt)</code>, with <code dir=\"ltr\">attempt</code> starting at 1, and resolve with its result.</li>\n<li>If it rejects, retry up to <code dir=\"ltr\">retries</code> more times. Before each retry, <code dir=\"ltr\">await sleep(ms)</code> where the delays double: <code dir=\"ltr\">baseDelayMs</code>, then <code dir=\"ltr\">2 × baseDelayMs</code>, then <code dir=\"ltr\">4 ×</code>… (for 100: 100, 200, 400).</li>\n<li>Errors with a numeric <code dir=\"ltr\">status</code> from 400 to 499 can’t succeed by retrying: rethrow them <strong>immediately</strong>. The exceptions are <code dir=\"ltr\">408</code> (timeout) and <code dir=\"ltr\">429</code> (too many requests), which are retried.</li>\n<li>Errors with no <code dir=\"ltr\">status</code> (network failures) and 5xx errors are retried.</li>\n<li>When every attempt fails, reject with the <strong>last</strong> error.</li>\n</ul>\n<p>Always wait with the <code dir=\"ltr\">sleep</code> you were given, so the checks can record the delays.</p>\n","hints":["A <code dir=\"ltr\">for (let attempt = 1; ; attempt++)</code> loop with <code dir=\"ltr\">try { return await task(attempt); } catch (error) { … }</code> inside reads more simply than recursion.","In the <code dir=\"ltr\">catch</code>, give up (<code dir=\"ltr\">throw error</code>) when <code dir=\"ltr\">attempt &gt; retries</code> or the error isn’t retryable; otherwise <code dir=\"ltr\">await sleep(baseDelayMs * 2 ** (attempt - 1))</code>.","Write <code dir=\"ltr\">isRetryable(error)</code> separately: no numeric status → true; 408 or 429 → true; otherwise retry only when <code dir=\"ltr\">status &gt;= 500</code>."],"explanation":"<p>Exponential backoff gives a brief signal drop time to recover without hammering a struggling server, and classifying errors keeps the app from retrying requests that will never work: a 404 or 401 fails the same way three times, just slower. Injecting <code dir=\"ltr\">sleep</code> is a small design choice with a big payoff: production uses a real timer, and tests run instantly while still checking the exact delays. Libraries like TanStack Query apply the same policy for you.</p>\n","checkLabels":{"returns-value":"A successful task resolves with its value, without waiting","retries-then-succeeds":"A task that fails twice then succeeds resolves","backoff":"Delays double between attempts (100, 200, 400)","gives-up":"After the last attempt, the last error is thrown","no-retry-4xx":"A 404 is thrown immediately without retrying","retry-429":"429 and 408 responses are retried"}},"words":661,"readMinutes":3}