{"id":"l-rn-2-4","courseId":"react-native-apps","lang":"en","kind":"lesson","summary":"Render Trailhead’s growing hike log with FlatList and SectionList, understand virtualization, and apply the handful of optimizations that keep long lists smooth on mid-range phones.","html":"<p>Trailhead’s log starts with a dozen hikes. A keen hiker logs three a week; after four years that’s over 600 rows, each with a thumbnail, a title and a few stats. Render that with <code dir=\"ltr\">ScrollView</code> and <code dir=\"ltr\">.map()</code>, and the app builds every row’s native views before showing the first frame. On a flagship phone you might not notice. On the mid-range Android phones most of the world uses, you get a blank screen and a stutter.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">FlatList: render what’s visible</h2>\n<p><code dir=\"ltr\">FlatList</code> takes your data and a function to render one item, and it only mounts the rows near the viewport:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">src/app/index.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\"> { FlatList, 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\"> { HikeRow } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '../components/HikeRow'</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\"> { useHikes } </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\"> HikeLog</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:#0550AE;--shiki-dark:#79C0FF\"> hikes</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useHikes</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\">  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\">hikes</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\">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\"> hike.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\">HikeRow</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\">item</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>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">      ItemSeparatorComponent</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\"> &#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\">{ height: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, backgroundColor: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'#e2e8e4'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, marginLeft: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">80</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>\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\">, 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\">>No hikes yet. Log your first one!&#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\">      contentContainerStyle</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ paddingBottom: </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>\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>Like ScrollView, a FlatList needs a bounded height, so its parent must give it <code dir=\"ltr\">flex: 1</code> (a screen in Expo Router already does). <code dir=\"ltr\">renderItem</code> receives <code dir=\"ltr\">{ item, index }</code>. <code dir=\"ltr\">ListHeaderComponent</code> and <code dir=\"ltr\">ListFooterComponent</code> let a summary card or a “load more” spinner scroll with the list, instead of wrapping the FlatList in a ScrollView, which defeats virtualization entirely and triggers a warning.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">How virtualization works</h2>\n<p>FlatList keeps a <strong>render window</strong>: the rows on screen plus a buffer above and below. As you scroll, rows entering the window mount and rows leaving it unmount, replaced by empty space of the right height.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 640 300\" role=\"img\" aria-labelledby=\"l-rn-2-4-t1\"><defs><marker id=\"arrow-l-rn-2-4-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-2-4-t1\">Of 600 hikes, only the rows on screen plus a buffer above and below are mounted; rows outside the window are replaced by blank space of the same height.</title>\n  <rect class=\"d-box\" x=\"40\" y=\"20\" width=\"220\" height=\"260\" rx=\"10\"/>\n  <text class=\"d-label-muted\" x=\"150\" y=\"44\" text-anchor=\"middle\">rows 1–90: blank space</text>\n  <rect class=\"d-box-accent\" x=\"56\" y=\"64\" width=\"188\" height=\"40\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"150\" y=\"89\" text-anchor=\"middle\">buffer (mounted)</text>\n  <rect class=\"d-box-primary\" x=\"56\" y=\"112\" width=\"188\" height=\"76\" rx=\"6\"/>\n  <text class=\"d-label-strong\" x=\"150\" y=\"155\" text-anchor=\"middle\">visible rows</text>\n  <rect class=\"d-box-accent\" x=\"56\" y=\"196\" width=\"188\" height=\"40\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"150\" y=\"221\" text-anchor=\"middle\">buffer (mounted)</text>\n  <text class=\"d-label-muted\" x=\"150\" y=\"264\" text-anchor=\"middle\">rows 320–600: blank space</text>\n  <rect class=\"d-box-success\" x=\"320\" y=\"100\" width=\"150\" height=\"100\" rx=\"18\"/>\n  <text class=\"d-label-strong\" x=\"395\" y=\"146\" text-anchor=\"middle\">phone screen</text>\n  <text class=\"d-label-muted\" x=\"395\" y=\"170\" text-anchor=\"middle\">viewport</text>\n  <path class=\"d-arrow\" d=\"M320 150 L252 150\" marker-end=\"url(#arrow-l-rn-2-4-1)\"/>\n  <text class=\"d-label\" x=\"500\" y=\"60\" text-anchor=\"start\">windowSize</text>\n  <text class=\"d-label-muted\" x=\"500\" y=\"80\" text-anchor=\"start\">default 21 screens</text>\n  <text class=\"d-label\" x=\"500\" y=\"240\" text-anchor=\"start\">initialNumToRender</text>\n  <text class=\"d-label-muted\" x=\"500\" y=\"260\" text-anchor=\"start\">default 10 rows</text>\n</svg><figcaption>FlatList keeps only a window of rows mounted</figcaption></figure>\n<p>Two consequences follow. First, <strong>keys must be stable</strong>: FlatList and React use them to know which row is which as rows come and go, so use an id from your data. Array indexes break the moment you insert a new hike at the top. Second, <strong>rows forget local state</strong> when they unmount. An “expanded” flag kept in <code dir=\"ltr\">useState</code> inside the row resets after a long scroll. Lift that state into the parent (<code dir=\"ltr\">expandedId</code>) or into your data.</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>Keying rows by index</span></p>\n<p>Without <code dir=\"ltr\">keyExtractor</code>, FlatList uses <code dir=\"ltr\">item.key</code>, then <code dir=\"ltr\">item.id</code>, and finally the array index. Suppose Trailhead’s sync API sent <code dir=\"ltr\">uuid</code> instead of <code dir=\"ltr\">id</code>: every row would silently be keyed by position, and passing <code dir=\"ltr\">(item, index) =&gt; String(index)</code> yourself does the same. Insert a new hike at the top and every row below it now has a different key: rows remount, images flicker, and per-row state jumps to the wrong hike. Always return the real identifier, whatever it’s called.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">Keeping it smooth</h2>\n<p>Most list jank comes from re-rendering rows that didn’t change, or doing heavy work inside them. In order of payoff:</p>\n<ol>\n<li><strong>Memoize the row.</strong> Export <code dir=\"ltr\">HikeRow</code> wrapped in <code dir=\"ltr\">memo</code>, so a row re-renders only when its <code dir=\"ltr\">hike</code> prop changes. (If your project enables the React Compiler, it memoizes for you.)</li>\n<li><strong>Keep <code dir=\"ltr\">renderItem</code> stable.</strong> An inline arrow function is fine with a memoized row, but don’t create new objects for props on every render, such as <code dir=\"ltr\">style={{…}}</code> or <code dir=\"ltr\">hike={{ ...item }}</code>, which defeat <code dir=\"ltr\">memo</code>.</li>\n<li><strong>No heavy work in rows.</strong> Format dates and compute stats once, when the data changes, not in every row’s render.</li>\n<li><strong>Right-sized images.</strong> A 4000-pixel photo shrunk into a 56-point thumbnail costs memory for every visible row. Request thumbnails from your API, or use <code dir=\"ltr\">expo-image</code> with caching.</li>\n<li><strong>Fixed heights? Use <code dir=\"ltr\">getItemLayout</code>.</strong> If every row is 72 points plus a 1-point separator, FlatList can skip measuring:</li>\n</ol>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"1\"><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:#0550AE;--shiki-dark:#79C0FF\"> ROW</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 73</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\">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\">  getItemLayout</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">_</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">index</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ({ length: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">ROW</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, offset: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">ROW</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> *</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> index, index })</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>For logs that live on a server, don’t fetch all 600 hikes at once. <code dir=\"ltr\">onEndReached</code> fires when the user scrolls near the bottom (how near is set by <code dir=\"ltr\">onEndReachedThreshold</code>, measured in screen heights), which is the moment to request the next page and append it to <code dir=\"ltr\">data</code>. Show a spinner in <code dir=\"ltr\">ListFooterComponent</code> while it loads, and guard against firing the request twice.</p>\n<p>Tuning <code dir=\"ltr\">windowSize</code> and <code dir=\"ltr\">initialNumToRender</code> comes last, and only after you’ve measured. For very large or complex feeds, many teams switch to Shopify’s FlashList, which recycles row views instead of mounting new ones and keeps a FlatList-like API.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">SectionList: grouped data</h2>\n<p>Trailhead groups hikes by month, with a sticky header for each. <code dir=\"ltr\">SectionList</code> does that from data shaped like this:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</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:#0550AE;--shiki-dark:#79C0FF\"> sections</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\">'October 2026'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, data: [ridgeLoop, lakeTrail] },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  { title: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'September 2026'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, data: [oldMill] },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">];</span></span></code></pre></div>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"3\"><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\">SectionList</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  sections</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">sections</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\">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\"> hike.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\">HikeRow</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\">item</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>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  renderSectionHeader</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">section</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\">styles.header</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\">section.title</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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  stickySectionHeadersEnabled</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">/></span></span></code></pre></div>\n<p>The interesting part isn’t the component; it’s turning a flat array of hikes into that <code dir=\"ltr\">sections</code> shape, correctly sorted. That’s your exercise.</p>\n<p>Section 3 starts next: with real screens to move between, Trailhead needs navigation.</p>\n","toc":[{"id":"sec-1","text":"FlatList: render what’s visible","level":2},{"id":"sec-2","text":"How virtualization works","level":2},{"id":"sec-3","text":"Keeping it smooth","level":2},{"id":"sec-4","text":"SectionList: grouped data","level":2}],"takeaways":["<code dir=\"ltr\">ScrollView</code> with <code dir=\"ltr\">.map()</code> mounts every row at once; <code dir=\"ltr\">FlatList</code> mounts only a window of rows around what’s visible.","Give <code dir=\"ltr\">FlatList</code> a <code dir=\"ltr\">keyExtractor</code> that returns a stable id from your data, never the array index.","Virtualized rows unmount when they scroll far away, so keep row state in your data, not inside the row component.","Memoize the row component, keep <code dir=\"ltr\">renderItem</code> stable, and add <code dir=\"ltr\">getItemLayout</code> when every row has the same height.","<code dir=\"ltr\">SectionList</code> takes <code dir=\"ltr\">sections</code> shaped as <code dir=\"ltr\">{ title, data }</code> and renders grouped lists with sticky headers."],"further":[{"title":"FlatList","url":"https://reactnative.dev/docs/flatlist"},{"title":"SectionList","url":"https://reactnative.dev/docs/sectionlist"},{"title":"Optimizing FlatList Configuration","url":"https://reactnative.dev/docs/optimizing-flatlist-configuration"}],"quiz":[{"q":"<p>Trailhead’s log has 600 hikes rendered with <code dir=\"ltr\">ScrollView</code> and <code dir=\"ltr\">.map()</code>. The screen takes three seconds to appear on an older Android phone. Why?</p>","options":[{"text":"ScrollView downloads every thumbnail before showing anything.","why":"Images load asynchronously either way. The cost is in creating 600 rows of native views up front."},{"text":"ScrollView is deprecated and runs in a compatibility mode.","why":"ScrollView isn’t deprecated; it’s the right choice for a screen of content. It’s the wrong choice for a long data list."},{"text":"ScrollView renders all 600 rows, with all their native views, before the first frame.","why":"Correct. FlatList would render roughly the first screenful, then fill in more as you scroll."}],"answer":2},{"q":"<p>Each hike row has a local <code dir=\"ltr\">useState</code> for “expanded”. Users expand a row, scroll far down, scroll back, and it’s collapsed again. Why?</p>","options":[{"text":"FlatList unmounted the row when it left the render window, and its local state went with it.","why":"Correct. Virtualization unmounts distant rows. Keep <code dir=\"ltr\">expandedId</code> in the parent or in your data and pass it down."},{"text":"The keyExtractor returns duplicate keys, so React reused the wrong row.","why":"Duplicate keys cause wrong rows to update, not state resets on scroll. React would also warn you."},{"text":"FlatList re-sorts the data whenever you scroll.","why":"FlatList never changes the order of your data. It only decides which rows to mount."}],"answer":0},{"q":"<p>Every Trailhead row is exactly 72 points tall. Which prop lets FlatList skip measuring rows and jump straight to row 400?</p>","options":[{"text":"<code dir=\"ltr\">initialNumToRender</code>","why":"That controls how many rows render in the first batch; it doesn’t tell FlatList anything about their size."},{"text":"<code dir=\"ltr\">getItemLayout</code>","why":"Correct. With <code dir=\"ltr\">length</code> and <code dir=\"ltr\">offset</code> known up front, FlatList doesn’t measure rows, and <code dir=\"ltr\">scrollToIndex</code> works for rows that aren’t rendered yet."},{"text":"<code dir=\"ltr\">removeClippedSubviews</code>","why":"That detaches off-screen views from the native hierarchy. It can save memory but says nothing about row sizes."},{"text":"<code dir=\"ltr\">windowSize</code>","why":"That controls how many screens of rows stay mounted around the viewport, not how they’re measured."}],"answer":1}],"exercise":{"kind":"web","mode":"js","starter":{"js":"const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];\n\nconst hikes = [\n  { id: 'h1', name: 'Ridge Loop', date: '2026-10-04', distanceKm: 8.4 },\n  { id: 'h2', name: 'Old Mill Creek', date: '2026-09-12', distanceKm: 5.1 },\n  { id: 'h3', name: 'Lake Trail', date: '2026-10-19', distanceKm: 11.2 },\n  { id: 'h4', name: 'Eagle Point', date: '2025-10-02', distanceKm: 14.0 },\n  { id: 'h5', name: 'Fern Gully', date: '2026-09-28', distanceKm: 3.6 },\n];\n\n// Return SectionList sections: [{ title: 'October 2026', data: [...] }, ...]\nfunction groupHikesByMonth(hikes) {\n  const sections = {};\n  for (const hike of hikes) {\n    const month = MONTHS[Number(hike.date.slice(5, 7))];\n    if (!sections[month]) sections[month] = [];\n    sections[month].push(hike);\n  }\n  return Object.keys(sections).map((title) => ({ title, data: sections[title] }));\n}\n\nconsole.log(JSON.stringify(groupHikesByMonth(hikes), null, 2));\n"},"solution":{"js":"const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];\n\nconst hikes = [\n  { id: 'h1', name: 'Ridge Loop', date: '2026-10-04', distanceKm: 8.4 },\n  { id: 'h2', name: 'Old Mill Creek', date: '2026-09-12', distanceKm: 5.1 },\n  { id: 'h3', name: 'Lake Trail', date: '2026-10-19', distanceKm: 11.2 },\n  { id: 'h4', name: 'Eagle Point', date: '2025-10-02', distanceKm: 14.0 },\n  { id: 'h5', name: 'Fern Gully', date: '2026-09-28', distanceKm: 3.6 },\n];\n\nfunction groupHikesByMonth(hikes) {\n  const sorted = [...hikes].sort((a, b) => b.date.localeCompare(a.date));\n  const sections = [];\n  for (const hike of sorted) {\n    const year = hike.date.slice(0, 4);\n    const month = MONTHS[Number(hike.date.slice(5, 7)) - 1];\n    const title = `${month} ${year}`;\n    const last = sections[sections.length - 1];\n    if (last && last.title === title) last.data.push(hike);\n    else sections.push({ title, data: [hike] });\n  }\n  return sections;\n}\n\nconsole.log(JSON.stringify(groupHikesByMonth(hikes), null, 2));\n"},"checks":[{"id":"titles","test":"const sample = [{ id: 'a', date: '2026-10-04' }, { id: 'b', date: '2026-09-12' }, { id: 'c', date: '2026-10-19' }, { id: 'd', date: '2025-10-02' }, { id: 'e', date: '2026-09-28' }]; const s = groupHikesByMonth(sample); return Array.isArray(s) && s.map((x) => x.title).join('|') === 'October 2026|September 2026|October 2025';"},{"id":"items-newest-first","test":"const sample = [{ id: 'a', date: '2026-10-04' }, { id: 'b', date: '2026-09-12' }, { id: 'c', date: '2026-10-19' }, { id: 'd', date: '2025-10-02' }, { id: 'e', date: '2026-09-28' }]; const s = groupHikesByMonth(sample); return s[0].data.map((h) => h.id).join() === 'c,a' && s[1].data.map((h) => h.id).join() === 'e,b' && s[2].data.map((h) => h.id).join() === 'd';"},{"id":"january-december","test":"const s = groupHikesByMonth([{ id: 'x', date: '2026-01-31' }, { id: 'y', date: '2025-12-01' }]); return s.length === 2 && s[0].title === 'January 2026' && s[1].title === 'December 2025';"},{"id":"no-mutation","test":"const sample = [{ id: 'a', date: '2026-10-04' }, { id: 'b', date: '2026-09-12' }, { id: 'c', date: '2026-10-19' }, { id: 'd', date: '2025-10-02' }, { id: 'e', date: '2026-09-28' }]; const ids = sample.map((h) => h.id).join(); groupHikesByMonth(sample); return sample.map((h) => h.id).join() === ids;"},{"id":"empty","test":"const s = groupHikesByMonth([]); return Array.isArray(s) && s.length === 0;"}],"title":"Group hikes into SectionList sections","prompt":"<p>Write <code dir=\"ltr\">groupHikesByMonth(hikes)</code> so Trailhead’s <code dir=\"ltr\">SectionList</code> can render the log grouped by month. Return an array of sections shaped <code dir=\"ltr\">{ title, data }</code> where:</p>\n<ul>\n<li><code dir=\"ltr\">title</code> is the month name and year, like <code dir=\"ltr\">&quot;October 2026&quot;</code> (use the <code dir=\"ltr\">MONTHS</code> array),</li>\n<li>sections are ordered <strong>newest month first</strong>, and October 2026 and October 2025 are different sections,</li>\n<li>hikes inside each section are ordered <strong>newest first</strong>,</li>\n<li>the input array is not modified.</li>\n</ul>\n<p>Dates are <code dir=\"ltr\">YYYY-MM-DD</code> strings. The starter has several bugs; run it and compare its output with what the list needs.</p>\n","hints":["<code dir=\"ltr\">MONTHS</code> is zero-based, but the month in <code dir=\"ltr\">'2026-10-04'</code> is 10. And the title needs the year too.","If you sort a copy of the hikes newest first (<code dir=\"ltr\">[...hikes].sort((a, b) =&gt; b.date.localeCompare(a.date))</code>), ISO date strings compare correctly as text.","After sorting, walk the hikes once: if the hike’s title matches the last section’s title, push into it; otherwise start a new section. The order of sections then falls out of the sort."],"explanation":"<p>The starter used the wrong month index (off by one), dropped the year so October 2025 merged into October 2026, and relied on object key order, which follows insertion rather than date. Sorting a copy first, then grouping consecutive hikes, solves ordering for both sections and items in one pass and leaves the caller’s array untouched. ISO <code dir=\"ltr\">YYYY-MM-DD</code> strings sort chronologically as plain text, which avoids time-zone surprises from <code dir=\"ltr\">new Date()</code>. Compute this when the data changes (for example in <code dir=\"ltr\">useMemo</code>), not inside <code dir=\"ltr\">renderItem</code>.</p>\n","checkLabels":{"titles":"Section titles include the year and are ordered newest month first","items-newest-first":"Hikes inside each section are ordered newest first","january-december":"January and December get the right month names","no-mutation":"The original hikes array keeps its order","empty":"An empty log gives an empty sections array"}},"words":676,"readMinutes":3}