[{"data":1,"prerenderedAt":5537},["ShallowReactive",2],{"page-\u002Freact\u002F13-lists-keys-and-reconciliation":3},{"id":4,"title":5,"body":6,"description":5530,"extension":5531,"meta":5532,"navigation":58,"path":5533,"seo":5534,"stem":5535,"__hash__":5536},"content\u002Freact\u002F13-lists-keys-and-reconciliation.md","13 — Lists, Keys & Reconciliation",{"type":7,"value":8,"toc":5515},"minimark",[9,13,18,542,546,867,871,1314,1318,1636,1640,2439,2443,3122,3129,3466,3470,4689,4693,4820,4824,4966,4970,4974,5300,5400,5404,5511],[10,11,5],"h1",{"id":12},"_13-lists-keys-reconciliation",[14,15,17],"h2",{"id":16},"the-reconciliation-algorithm","The Reconciliation Algorithm",[19,20,23],"code-wrapper",{"filename":21,"language":22},"reconciliation_overview.js","javascript",[24,25,29],"pre",{"className":26,"code":27,"language":22,"meta":28,"style":28},"language-javascript shiki shiki-themes github-light github-dark","\u002F\u002F When a component re-renders, React does NOT diff real DOM nodes.\n\u002F\u002F It diffs the NEW React element tree against the PREVIOUS render's element tree,\n\u002F\u002F then computes the minimal set of real DOM mutations to apply.\n\n\u002F\u002F This process is called RECONCILIATION. It uses a heuristic algorithm, NOT\n\u002F\u002F a general tree-diff (a general tree-diff is O(n³) — far too slow for UIs).\n\n\u002F\u002F React's heuristic runs in O(n) via TWO assumptions:\n\u002F\u002F\n\u002F\u002F 1. DIFFERENT element types → different trees\n\u002F\u002F    \u003Cdiv> → \u003Cspan> : React tears down the entire old subtree, builds a new one.\n\u002F\u002F    No reuse, no diffing of children. Full unmount + remount.\n\u002F\u002F\n\u002F\u002F 2. SAME element type + SAME position → same underlying thing\n\u002F\u002F    \u003Cdiv className=\"a\"> → \u003Cdiv className=\"b\"> : React keeps the DOM node,\n\u002F\u002F    just updates changed attributes (className, style, etc.) in place.\n\u002F\u002F    Children are then diffed recursively.\n\n\u002F\u002F The \"position\" assumption is where lists get dangerous:\n\u002F\u002F In an array of siblings, \"position\" is ambiguous without keys.\n\u002F\u002F That ambiguity is what the `key` prop exists to resolve.\n\n\u002F\u002F ── Annotated diffing pseudocode (what React does internally) ──\n\nfunction reconcileChildren(prevChildren, nextChildren) {\n  \u002F\u002F prevChildren: [elementA, elementB, elementC]  (from last render)\n  \u002F\u002F nextChildren: [elementB, elementC, elementD]  (from this render, A removed, D added)\n\n  \u002F\u002F STEP 1: If nextChildren has keys, match by key first\n  const prevByKey = new Map(prevChildren.map(c => [c.key, c]))\n  const matched = []\n  const toCreate = []\n\n  for (const nextChild of nextChildren) {\n    if (nextChild.key !== null && prevByKey.has(nextChild.key)) {\n      \u002F\u002F Key match: reuse the previous element's DOM node + state\n      matched.push(reuseAndUpdate(prevByKey.get(nextChild.key), nextChild))\n      prevByKey.delete(nextChild.key)\n    } else {\n      \u002F\u002F No key match: this is a new element → create from scratch\n      toCreate.push(nextChild)\n    }\n  }\n\n  \u002F\u002F STEP 2: Any prev elements not matched by key → unmount (destroy DOM + state)\n  for (const [, unmatched] of prevByKey) {\n    unmount(unmatched)  \u002F\u002F cleanup effects, remove DOM nodes, destroy state\n  }\n\n  \u002F\u002F STEP 3: Create DOM for new elements\n  for (const newChild of toCreate) {\n    mount(newChild)\n  }\n\n  \u002F\u002F STEP 4: Reorder matched DOM nodes into the correct positions\n  reorder(matched, nextChildren)\n}\n","",[30,31,32,41,47,53,60,66,72,77,83,89,95,101,107,112,118,124,130,136,141,147,153,159,164,170,175,203,209,215,220,226,262,275,287,292,313,340,346,369,381,393,399,410,416,422,427,433,457,469,474,479,485,502,511,516,521,527,536],"code",{"__ignoreMap":28},[33,34,37],"span",{"class":35,"line":36},"line",1,[33,38,40],{"class":39},"sdCPZ","\u002F\u002F When a component re-renders, React does NOT diff real DOM nodes.\n",[33,42,44],{"class":35,"line":43},2,[33,45,46],{"class":39},"\u002F\u002F It diffs the NEW React element tree against the PREVIOUS render's element tree,\n",[33,48,50],{"class":35,"line":49},3,[33,51,52],{"class":39},"\u002F\u002F then computes the minimal set of real DOM mutations to apply.\n",[33,54,56],{"class":35,"line":55},4,[33,57,59],{"emptyLinePlaceholder":58},true,"\n",[33,61,63],{"class":35,"line":62},5,[33,64,65],{"class":39},"\u002F\u002F This process is called RECONCILIATION. It uses a heuristic algorithm, NOT\n",[33,67,69],{"class":35,"line":68},6,[33,70,71],{"class":39},"\u002F\u002F a general tree-diff (a general tree-diff is O(n³) — far too slow for UIs).\n",[33,73,75],{"class":35,"line":74},7,[33,76,59],{"emptyLinePlaceholder":58},[33,78,80],{"class":35,"line":79},8,[33,81,82],{"class":39},"\u002F\u002F React's heuristic runs in O(n) via TWO assumptions:\n",[33,84,86],{"class":35,"line":85},9,[33,87,88],{"class":39},"\u002F\u002F\n",[33,90,92],{"class":35,"line":91},10,[33,93,94],{"class":39},"\u002F\u002F 1. DIFFERENT element types → different trees\n",[33,96,98],{"class":35,"line":97},11,[33,99,100],{"class":39},"\u002F\u002F    \u003Cdiv> → \u003Cspan> : React tears down the entire old subtree, builds a new one.\n",[33,102,104],{"class":35,"line":103},12,[33,105,106],{"class":39},"\u002F\u002F    No reuse, no diffing of children. Full unmount + remount.\n",[33,108,110],{"class":35,"line":109},13,[33,111,88],{"class":39},[33,113,115],{"class":35,"line":114},14,[33,116,117],{"class":39},"\u002F\u002F 2. SAME element type + SAME position → same underlying thing\n",[33,119,121],{"class":35,"line":120},15,[33,122,123],{"class":39},"\u002F\u002F    \u003Cdiv className=\"a\"> → \u003Cdiv className=\"b\"> : React keeps the DOM node,\n",[33,125,127],{"class":35,"line":126},16,[33,128,129],{"class":39},"\u002F\u002F    just updates changed attributes (className, style, etc.) in place.\n",[33,131,133],{"class":35,"line":132},17,[33,134,135],{"class":39},"\u002F\u002F    Children are then diffed recursively.\n",[33,137,139],{"class":35,"line":138},18,[33,140,59],{"emptyLinePlaceholder":58},[33,142,144],{"class":35,"line":143},19,[33,145,146],{"class":39},"\u002F\u002F The \"position\" assumption is where lists get dangerous:\n",[33,148,150],{"class":35,"line":149},20,[33,151,152],{"class":39},"\u002F\u002F In an array of siblings, \"position\" is ambiguous without keys.\n",[33,154,156],{"class":35,"line":155},21,[33,157,158],{"class":39},"\u002F\u002F That ambiguity is what the `key` prop exists to resolve.\n",[33,160,162],{"class":35,"line":161},22,[33,163,59],{"emptyLinePlaceholder":58},[33,165,167],{"class":35,"line":166},23,[33,168,169],{"class":39},"\u002F\u002F ── Annotated diffing pseudocode (what React does internally) ──\n",[33,171,173],{"class":35,"line":172},24,[33,174,59],{"emptyLinePlaceholder":58},[33,176,178,182,186,190,194,197,200],{"class":35,"line":177},25,[33,179,181],{"class":180},"svdQ7","function",[33,183,185],{"class":184},"sIsaT"," reconcileChildren",[33,187,189],{"class":188},"ssxIu","(",[33,191,193],{"class":192},"sCrzJ","prevChildren",[33,195,196],{"class":188},", ",[33,198,199],{"class":192},"nextChildren",[33,201,202],{"class":188},") {\n",[33,204,206],{"class":35,"line":205},26,[33,207,208],{"class":39},"  \u002F\u002F prevChildren: [elementA, elementB, elementC]  (from last render)\n",[33,210,212],{"class":35,"line":211},27,[33,213,214],{"class":39},"  \u002F\u002F nextChildren: [elementB, elementC, elementD]  (from this render, A removed, D added)\n",[33,216,218],{"class":35,"line":217},28,[33,219,59],{"emptyLinePlaceholder":58},[33,221,223],{"class":35,"line":222},29,[33,224,225],{"class":39},"  \u002F\u002F STEP 1: If nextChildren has keys, match by key first\n",[33,227,229,232,236,239,242,245,248,251,253,256,259],{"class":35,"line":228},30,[33,230,231],{"class":180},"  const",[33,233,235],{"class":234},"snvgF"," prevByKey",[33,237,238],{"class":180}," =",[33,240,241],{"class":180}," new",[33,243,244],{"class":184}," Map",[33,246,247],{"class":188},"(prevChildren.",[33,249,250],{"class":184},"map",[33,252,189],{"class":188},[33,254,255],{"class":192},"c",[33,257,258],{"class":180}," =>",[33,260,261],{"class":188}," [c.key, c]))\n",[33,263,265,267,270,272],{"class":35,"line":264},31,[33,266,231],{"class":180},[33,268,269],{"class":234}," matched",[33,271,238],{"class":180},[33,273,274],{"class":188}," []\n",[33,276,278,280,283,285],{"class":35,"line":277},32,[33,279,231],{"class":180},[33,281,282],{"class":234}," toCreate",[33,284,238],{"class":180},[33,286,274],{"class":188},[33,288,290],{"class":35,"line":289},33,[33,291,59],{"emptyLinePlaceholder":58},[33,293,295,298,301,304,307,310],{"class":35,"line":294},34,[33,296,297],{"class":180},"  for",[33,299,300],{"class":188}," (",[33,302,303],{"class":180},"const",[33,305,306],{"class":234}," nextChild",[33,308,309],{"class":180}," of",[33,311,312],{"class":188}," nextChildren) {\n",[33,314,316,319,322,325,328,331,334,337],{"class":35,"line":315},35,[33,317,318],{"class":180},"    if",[33,320,321],{"class":188}," (nextChild.key ",[33,323,324],{"class":180},"!==",[33,326,327],{"class":234}," null",[33,329,330],{"class":180}," &&",[33,332,333],{"class":188}," prevByKey.",[33,335,336],{"class":184},"has",[33,338,339],{"class":188},"(nextChild.key)) {\n",[33,341,343],{"class":35,"line":342},36,[33,344,345],{"class":39},"      \u002F\u002F Key match: reuse the previous element's DOM node + state\n",[33,347,349,352,355,357,360,363,366],{"class":35,"line":348},37,[33,350,351],{"class":188},"      matched.",[33,353,354],{"class":184},"push",[33,356,189],{"class":188},[33,358,359],{"class":184},"reuseAndUpdate",[33,361,362],{"class":188},"(prevByKey.",[33,364,365],{"class":184},"get",[33,367,368],{"class":188},"(nextChild.key), nextChild))\n",[33,370,372,375,378],{"class":35,"line":371},38,[33,373,374],{"class":188},"      prevByKey.",[33,376,377],{"class":184},"delete",[33,379,380],{"class":188},"(nextChild.key)\n",[33,382,384,387,390],{"class":35,"line":383},39,[33,385,386],{"class":188},"    } ",[33,388,389],{"class":180},"else",[33,391,392],{"class":188}," {\n",[33,394,396],{"class":35,"line":395},40,[33,397,398],{"class":39},"      \u002F\u002F No key match: this is a new element → create from scratch\n",[33,400,402,405,407],{"class":35,"line":401},41,[33,403,404],{"class":188},"      toCreate.",[33,406,354],{"class":184},[33,408,409],{"class":188},"(nextChild)\n",[33,411,413],{"class":35,"line":412},42,[33,414,415],{"class":188},"    }\n",[33,417,419],{"class":35,"line":418},43,[33,420,421],{"class":188},"  }\n",[33,423,425],{"class":35,"line":424},44,[33,426,59],{"emptyLinePlaceholder":58},[33,428,430],{"class":35,"line":429},45,[33,431,432],{"class":39},"  \u002F\u002F STEP 2: Any prev elements not matched by key → unmount (destroy DOM + state)\n",[33,434,436,438,440,442,445,448,451,454],{"class":35,"line":435},46,[33,437,297],{"class":180},[33,439,300],{"class":188},[33,441,303],{"class":180},[33,443,444],{"class":188}," [, ",[33,446,447],{"class":234},"unmatched",[33,449,450],{"class":188},"] ",[33,452,453],{"class":180},"of",[33,455,456],{"class":188}," prevByKey) {\n",[33,458,460,463,466],{"class":35,"line":459},47,[33,461,462],{"class":184},"    unmount",[33,464,465],{"class":188},"(unmatched)  ",[33,467,468],{"class":39},"\u002F\u002F cleanup effects, remove DOM nodes, destroy state\n",[33,470,472],{"class":35,"line":471},48,[33,473,421],{"class":188},[33,475,477],{"class":35,"line":476},49,[33,478,59],{"emptyLinePlaceholder":58},[33,480,482],{"class":35,"line":481},50,[33,483,484],{"class":39},"  \u002F\u002F STEP 3: Create DOM for new elements\n",[33,486,488,490,492,494,497,499],{"class":35,"line":487},51,[33,489,297],{"class":180},[33,491,300],{"class":188},[33,493,303],{"class":180},[33,495,496],{"class":234}," newChild",[33,498,309],{"class":180},[33,500,501],{"class":188}," toCreate) {\n",[33,503,505,508],{"class":35,"line":504},52,[33,506,507],{"class":184},"    mount",[33,509,510],{"class":188},"(newChild)\n",[33,512,514],{"class":35,"line":513},53,[33,515,421],{"class":188},[33,517,519],{"class":35,"line":518},54,[33,520,59],{"emptyLinePlaceholder":58},[33,522,524],{"class":35,"line":523},55,[33,525,526],{"class":39},"  \u002F\u002F STEP 4: Reorder matched DOM nodes into the correct positions\n",[33,528,530,533],{"class":35,"line":529},56,[33,531,532],{"class":184},"  reorder",[33,534,535],{"class":188},"(matched, nextChildren)\n",[33,537,539],{"class":35,"line":538},57,[33,540,541],{"class":188},"}\n",[14,543,545],{"id":544},"why-keys-exist-the-identity-problem","Why Keys Exist: The Identity Problem",[19,547,549],{"filename":548,"language":22},"identity_problem.js",[24,550,552],{"className":26,"code":551,"language":22,"meta":28,"style":28},"import { useState } from 'react'\n\n\u002F\u002F Without keys, React's default matching strategy for array children is POSITIONAL:\n\u002F\u002F new index-0 matches old index-0, new index-1 matches old index-1, etc.\n\u002F\u002F regardless of what the items actually represent.\n\n\u002F\u002F ANTI-PATTERN: no key prop (React warns in dev, falls back to positional matching)\nfunction TodoListNoKey({ todos }) {\n  return (\n    \u003Cul>\n      {todos.map(todo => \u003Cli>{todo.text}\u003C\u002Fli>)}\n      {\u002F* React console warning: \"Each child in a list should have a unique key prop\" *\u002F}\n    \u003C\u002Ful>\n  )\n}\n\n\u002F\u002F THE BUG this causes:\n\u002F\u002F todos = [{ id: 1, text: 'Buy milk' }, { id: 2, text: 'Walk dog' }]\n\u002F\u002F User deletes \"Buy milk\" → todos = [{ id: 2, text: 'Walk dog' }]\n\u002F\u002F\n\u002F\u002F Positional matching:\n\u002F\u002F   old index-0 = \"Buy milk\"  →  new index-0 = \"Walk dog\"\n\u002F\u002F   React sees: same type (li), same position (0) → \"same thing, update in place\"\n\u002F\u002F   React reuses the old \u003Cli> DOM node and just updates its text content.\n\u002F\u002F   It does NOT realize \"Buy milk\" was removed and \"Walk dog\" shifted up.\n\u002F\u002F\n\u002F\u002F Any per-item state (uncontrolled inputs, local useState, CSS transitions) attached\n\u002F\u002F to that \u003Cli>'s subtree stays attached to the POSITION, not the ITEM.\n\u002F\u002F Result: state ends up displaying against the wrong data.\n\n\u002F\u002F CORRECT: stable unique key from the data\nfunction TodoListKeyed({ todos }) {\n  return (\n    \u003Cul>\n      {todos.map(todo => (\n        \u003Cli key={todo.id}>{todo.text}\u003C\u002Fli>\n      ))}\n    \u003C\u002Ful>\n  )\n}\n\u002F\u002F Now deleting id:1 is unambiguous:\n\u002F\u002F   React sees key \"1\" is gone → removes exactly that \u003Cli> DOM node.\n\u002F\u002F   Key \"2\" is still present → its \u003Cli> is left completely untouched.\n\u002F\u002F   No update, no re-render, any state inside survives intact.\n",[30,553,554,569,573,578,583,588,592,597,613,621,633,661,671,680,685,689,693,698,703,708,712,717,722,727,732,737,741,746,751,756,760,765,778,784,792,806,826,831,839,843,847,852,857,862],{"__ignoreMap":28},[33,555,556,559,562,565],{"class":35,"line":36},[33,557,558],{"class":180},"import",[33,560,561],{"class":188}," { useState } ",[33,563,564],{"class":180},"from",[33,566,568],{"class":567},"sJ6F3"," 'react'\n",[33,570,571],{"class":35,"line":43},[33,572,59],{"emptyLinePlaceholder":58},[33,574,575],{"class":35,"line":49},[33,576,577],{"class":39},"\u002F\u002F Without keys, React's default matching strategy for array children is POSITIONAL:\n",[33,579,580],{"class":35,"line":55},[33,581,582],{"class":39},"\u002F\u002F new index-0 matches old index-0, new index-1 matches old index-1, etc.\n",[33,584,585],{"class":35,"line":62},[33,586,587],{"class":39},"\u002F\u002F regardless of what the items actually represent.\n",[33,589,590],{"class":35,"line":68},[33,591,59],{"emptyLinePlaceholder":58},[33,593,594],{"class":35,"line":74},[33,595,596],{"class":39},"\u002F\u002F ANTI-PATTERN: no key prop (React warns in dev, falls back to positional matching)\n",[33,598,599,601,604,607,610],{"class":35,"line":79},[33,600,181],{"class":180},[33,602,603],{"class":184}," TodoListNoKey",[33,605,606],{"class":188},"({ ",[33,608,609],{"class":192},"todos",[33,611,612],{"class":188}," }) {\n",[33,614,615,618],{"class":35,"line":85},[33,616,617],{"class":180},"  return",[33,619,620],{"class":188}," (\n",[33,622,623,626,630],{"class":35,"line":91},[33,624,625],{"class":188},"    \u003C",[33,627,629],{"class":628},"sk71V","ul",[33,631,632],{"class":188},">\n",[33,634,635,638,640,642,645,647,650,653,656,658],{"class":35,"line":97},[33,636,637],{"class":188},"      {todos.",[33,639,250],{"class":184},[33,641,189],{"class":188},[33,643,644],{"class":192},"todo",[33,646,258],{"class":180},[33,648,649],{"class":188}," \u003C",[33,651,652],{"class":628},"li",[33,654,655],{"class":188},">{todo.text}\u003C\u002F",[33,657,652],{"class":628},[33,659,660],{"class":188},">)}\n",[33,662,663,666,669],{"class":35,"line":103},[33,664,665],{"class":188},"      {",[33,667,668],{"class":39},"\u002F* React console warning: \"Each child in a list should have a unique key prop\" *\u002F",[33,670,541],{"class":188},[33,672,673,676,678],{"class":35,"line":109},[33,674,675],{"class":188},"    \u003C\u002F",[33,677,629],{"class":628},[33,679,632],{"class":188},[33,681,682],{"class":35,"line":114},[33,683,684],{"class":188},"  )\n",[33,686,687],{"class":35,"line":120},[33,688,541],{"class":188},[33,690,691],{"class":35,"line":126},[33,692,59],{"emptyLinePlaceholder":58},[33,694,695],{"class":35,"line":132},[33,696,697],{"class":39},"\u002F\u002F THE BUG this causes:\n",[33,699,700],{"class":35,"line":138},[33,701,702],{"class":39},"\u002F\u002F todos = [{ id: 1, text: 'Buy milk' }, { id: 2, text: 'Walk dog' }]\n",[33,704,705],{"class":35,"line":143},[33,706,707],{"class":39},"\u002F\u002F User deletes \"Buy milk\" → todos = [{ id: 2, text: 'Walk dog' }]\n",[33,709,710],{"class":35,"line":149},[33,711,88],{"class":39},[33,713,714],{"class":35,"line":155},[33,715,716],{"class":39},"\u002F\u002F Positional matching:\n",[33,718,719],{"class":35,"line":161},[33,720,721],{"class":39},"\u002F\u002F   old index-0 = \"Buy milk\"  →  new index-0 = \"Walk dog\"\n",[33,723,724],{"class":35,"line":166},[33,725,726],{"class":39},"\u002F\u002F   React sees: same type (li), same position (0) → \"same thing, update in place\"\n",[33,728,729],{"class":35,"line":172},[33,730,731],{"class":39},"\u002F\u002F   React reuses the old \u003Cli> DOM node and just updates its text content.\n",[33,733,734],{"class":35,"line":177},[33,735,736],{"class":39},"\u002F\u002F   It does NOT realize \"Buy milk\" was removed and \"Walk dog\" shifted up.\n",[33,738,739],{"class":35,"line":205},[33,740,88],{"class":39},[33,742,743],{"class":35,"line":211},[33,744,745],{"class":39},"\u002F\u002F Any per-item state (uncontrolled inputs, local useState, CSS transitions) attached\n",[33,747,748],{"class":35,"line":217},[33,749,750],{"class":39},"\u002F\u002F to that \u003Cli>'s subtree stays attached to the POSITION, not the ITEM.\n",[33,752,753],{"class":35,"line":222},[33,754,755],{"class":39},"\u002F\u002F Result: state ends up displaying against the wrong data.\n",[33,757,758],{"class":35,"line":228},[33,759,59],{"emptyLinePlaceholder":58},[33,761,762],{"class":35,"line":264},[33,763,764],{"class":39},"\u002F\u002F CORRECT: stable unique key from the data\n",[33,766,767,769,772,774,776],{"class":35,"line":277},[33,768,181],{"class":180},[33,770,771],{"class":184}," TodoListKeyed",[33,773,606],{"class":188},[33,775,609],{"class":192},[33,777,612],{"class":188},[33,779,780,782],{"class":35,"line":289},[33,781,617],{"class":180},[33,783,620],{"class":188},[33,785,786,788,790],{"class":35,"line":294},[33,787,625],{"class":188},[33,789,629],{"class":628},[33,791,632],{"class":188},[33,793,794,796,798,800,802,804],{"class":35,"line":315},[33,795,637],{"class":188},[33,797,250],{"class":184},[33,799,189],{"class":188},[33,801,644],{"class":192},[33,803,258],{"class":180},[33,805,620],{"class":188},[33,807,808,811,813,816,819,822,824],{"class":35,"line":342},[33,809,810],{"class":188},"        \u003C",[33,812,652],{"class":628},[33,814,815],{"class":184}," key",[33,817,818],{"class":180},"=",[33,820,821],{"class":188},"{todo.id}>{todo.text}\u003C\u002F",[33,823,652],{"class":628},[33,825,632],{"class":188},[33,827,828],{"class":35,"line":348},[33,829,830],{"class":188},"      ))}\n",[33,832,833,835,837],{"class":35,"line":371},[33,834,675],{"class":188},[33,836,629],{"class":628},[33,838,632],{"class":188},[33,840,841],{"class":35,"line":383},[33,842,684],{"class":188},[33,844,845],{"class":35,"line":395},[33,846,541],{"class":188},[33,848,849],{"class":35,"line":401},[33,850,851],{"class":39},"\u002F\u002F Now deleting id:1 is unambiguous:\n",[33,853,854],{"class":35,"line":412},[33,855,856],{"class":39},"\u002F\u002F   React sees key \"1\" is gone → removes exactly that \u003Cli> DOM node.\n",[33,858,859],{"class":35,"line":418},[33,860,861],{"class":39},"\u002F\u002F   Key \"2\" is still present → its \u003Cli> is left completely untouched.\n",[33,863,864],{"class":35,"line":424},[33,865,866],{"class":39},"\u002F\u002F   No update, no re-render, any state inside survives intact.\n",[14,868,870],{"id":869},"the-index-as-key-trap","The Index-as-Key Trap",[19,872,874],{"filename":873,"language":22},"index_key_trap.js",[24,875,877],{"className":26,"code":876,"language":22,"meta":28,"style":28},"import { useState } from 'react'\n\n\u002F\u002F ANTI-PATTERN: using array index as key for a mutable list\nfunction EditableTodoListBad({ todos, onRemove }) {\n  return (\n    \u003Cul>\n      {todos.map((todo, index) => (\n        \u003Cli key={index}>\n          {\u002F* Uncontrolled checkbox — its checked state lives in the DOM, not React *\u002F}\n          \u003Cinput type=\"checkbox\" defaultChecked={todo.done} \u002F>\n          \u003Cspan>{todo.text}\u003C\u002Fspan>\n          \u003Cbutton onClick={() => onRemove(todo.id)}>Delete\u003C\u002Fbutton>\n        \u003C\u002Fli>\n      ))}\n    \u003C\u002Ful>\n  )\n}\n\u002F\u002F THE BUG:\n\u002F\u002F todos = [\n\u002F\u002F   { id: 1, text: 'Task A', done: true },   ← index 0, checkbox CHECKED\n\u002F\u002F   { id: 2, text: 'Task B', done: false },  ← index 1, checkbox UNCHECKED\n\u002F\u002F ]\n\u002F\u002F User deletes Task A → todos = [{ id: 2, text: 'Task B', done: false }]\n\u002F\u002F\n\u002F\u002F React matches by key=index:\n\u002F\u002F   old key=0 (Task A, checkbox checked) → new key=0 (Task B, done: false)\n\u002F\u002F   React: \"same key, same type → reuse DOM node, update props\"\n\u002F\u002F   The \u003Cli> DOM node is reused. The checkbox DOM node is reused.\n\u002F\u002F   defaultChecked={todo.done} = false is set, BUT defaultChecked only applies\n\u002F\u002F   on initial mount — it does NOT re-apply on reuse. The checkbox stays CHECKED\n\u002F\u002F   (from Task A) even though Task B's done is false.\n\u002F\u002F\n\u002F\u002F The checkbox visually shows the WRONG state for Task B.\n\n\u002F\u002F CORRECT: key by stable identity\nfunction EditableTodoListGood({ todos, onRemove }) {\n  return (\n    \u003Cul>\n      {todos.map(todo => (\n        \u003Cli key={todo.id}>\n          \u003Cinput type=\"checkbox\" defaultChecked={todo.done} \u002F>\n          \u003Cspan>{todo.text}\u003C\u002Fspan>\n          \u003Cbutton onClick={() => onRemove(todo.id)}>Delete\u003C\u002Fbutton>\n        \u003C\u002Fli>\n      ))}\n    \u003C\u002Ful>\n  )\n}\n\u002F\u002F Deleting Task A (id:1):\n\u002F\u002F   React sees key \"1\" is gone → removes that \u003Cli> entirely (including its checkbox).\n\u002F\u002F   Key \"2\" is still present → its \u003Cli> + checkbox are untouched.\n\u002F\u002F   Checkbox state stays correctly paired with its todo. ✓\n",[30,878,879,889,893,898,916,922,930,954,967,977,1001,1013,1040,1049,1053,1061,1065,1069,1074,1079,1084,1089,1094,1099,1103,1108,1113,1118,1123,1128,1133,1138,1142,1147,1151,1156,1173,1179,1187,1201,1214,1232,1244,1266,1274,1278,1286,1290,1294,1299,1304,1309],{"__ignoreMap":28},[33,880,881,883,885,887],{"class":35,"line":36},[33,882,558],{"class":180},[33,884,561],{"class":188},[33,886,564],{"class":180},[33,888,568],{"class":567},[33,890,891],{"class":35,"line":43},[33,892,59],{"emptyLinePlaceholder":58},[33,894,895],{"class":35,"line":49},[33,896,897],{"class":39},"\u002F\u002F ANTI-PATTERN: using array index as key for a mutable list\n",[33,899,900,902,905,907,909,911,914],{"class":35,"line":55},[33,901,181],{"class":180},[33,903,904],{"class":184}," EditableTodoListBad",[33,906,606],{"class":188},[33,908,609],{"class":192},[33,910,196],{"class":188},[33,912,913],{"class":192},"onRemove",[33,915,612],{"class":188},[33,917,918,920],{"class":35,"line":62},[33,919,617],{"class":180},[33,921,620],{"class":188},[33,923,924,926,928],{"class":35,"line":68},[33,925,625],{"class":188},[33,927,629],{"class":628},[33,929,632],{"class":188},[33,931,932,934,936,939,941,943,946,949,952],{"class":35,"line":74},[33,933,637],{"class":188},[33,935,250],{"class":184},[33,937,938],{"class":188},"((",[33,940,644],{"class":192},[33,942,196],{"class":188},[33,944,945],{"class":192},"index",[33,947,948],{"class":188},") ",[33,950,951],{"class":180},"=>",[33,953,620],{"class":188},[33,955,956,958,960,962,964],{"class":35,"line":79},[33,957,810],{"class":188},[33,959,652],{"class":628},[33,961,815],{"class":184},[33,963,818],{"class":180},[33,965,966],{"class":188},"{index}>\n",[33,968,969,972,975],{"class":35,"line":85},[33,970,971],{"class":188},"          {",[33,973,974],{"class":39},"\u002F* Uncontrolled checkbox — its checked state lives in the DOM, not React *\u002F",[33,976,541],{"class":188},[33,978,979,982,985,988,990,993,996,998],{"class":35,"line":91},[33,980,981],{"class":188},"          \u003C",[33,983,984],{"class":628},"input",[33,986,987],{"class":184}," type",[33,989,818],{"class":180},[33,991,992],{"class":567},"\"checkbox\"",[33,994,995],{"class":184}," defaultChecked",[33,997,818],{"class":180},[33,999,1000],{"class":188},"{todo.done} \u002F>\n",[33,1002,1003,1005,1007,1009,1011],{"class":35,"line":97},[33,1004,981],{"class":188},[33,1006,33],{"class":628},[33,1008,655],{"class":188},[33,1010,33],{"class":628},[33,1012,632],{"class":188},[33,1014,1015,1017,1020,1023,1025,1028,1030,1033,1036,1038],{"class":35,"line":103},[33,1016,981],{"class":188},[33,1018,1019],{"class":628},"button",[33,1021,1022],{"class":184}," onClick",[33,1024,818],{"class":180},[33,1026,1027],{"class":188},"{() ",[33,1029,951],{"class":180},[33,1031,1032],{"class":184}," onRemove",[33,1034,1035],{"class":188},"(todo.id)}>Delete\u003C\u002F",[33,1037,1019],{"class":628},[33,1039,632],{"class":188},[33,1041,1042,1045,1047],{"class":35,"line":109},[33,1043,1044],{"class":188},"        \u003C\u002F",[33,1046,652],{"class":628},[33,1048,632],{"class":188},[33,1050,1051],{"class":35,"line":114},[33,1052,830],{"class":188},[33,1054,1055,1057,1059],{"class":35,"line":120},[33,1056,675],{"class":188},[33,1058,629],{"class":628},[33,1060,632],{"class":188},[33,1062,1063],{"class":35,"line":126},[33,1064,684],{"class":188},[33,1066,1067],{"class":35,"line":132},[33,1068,541],{"class":188},[33,1070,1071],{"class":35,"line":138},[33,1072,1073],{"class":39},"\u002F\u002F THE BUG:\n",[33,1075,1076],{"class":35,"line":143},[33,1077,1078],{"class":39},"\u002F\u002F todos = [\n",[33,1080,1081],{"class":35,"line":149},[33,1082,1083],{"class":39},"\u002F\u002F   { id: 1, text: 'Task A', done: true },   ← index 0, checkbox CHECKED\n",[33,1085,1086],{"class":35,"line":155},[33,1087,1088],{"class":39},"\u002F\u002F   { id: 2, text: 'Task B', done: false },  ← index 1, checkbox UNCHECKED\n",[33,1090,1091],{"class":35,"line":161},[33,1092,1093],{"class":39},"\u002F\u002F ]\n",[33,1095,1096],{"class":35,"line":166},[33,1097,1098],{"class":39},"\u002F\u002F User deletes Task A → todos = [{ id: 2, text: 'Task B', done: false }]\n",[33,1100,1101],{"class":35,"line":172},[33,1102,88],{"class":39},[33,1104,1105],{"class":35,"line":177},[33,1106,1107],{"class":39},"\u002F\u002F React matches by key=index:\n",[33,1109,1110],{"class":35,"line":205},[33,1111,1112],{"class":39},"\u002F\u002F   old key=0 (Task A, checkbox checked) → new key=0 (Task B, done: false)\n",[33,1114,1115],{"class":35,"line":211},[33,1116,1117],{"class":39},"\u002F\u002F   React: \"same key, same type → reuse DOM node, update props\"\n",[33,1119,1120],{"class":35,"line":217},[33,1121,1122],{"class":39},"\u002F\u002F   The \u003Cli> DOM node is reused. The checkbox DOM node is reused.\n",[33,1124,1125],{"class":35,"line":222},[33,1126,1127],{"class":39},"\u002F\u002F   defaultChecked={todo.done} = false is set, BUT defaultChecked only applies\n",[33,1129,1130],{"class":35,"line":228},[33,1131,1132],{"class":39},"\u002F\u002F   on initial mount — it does NOT re-apply on reuse. The checkbox stays CHECKED\n",[33,1134,1135],{"class":35,"line":264},[33,1136,1137],{"class":39},"\u002F\u002F   (from Task A) even though Task B's done is false.\n",[33,1139,1140],{"class":35,"line":277},[33,1141,88],{"class":39},[33,1143,1144],{"class":35,"line":289},[33,1145,1146],{"class":39},"\u002F\u002F The checkbox visually shows the WRONG state for Task B.\n",[33,1148,1149],{"class":35,"line":294},[33,1150,59],{"emptyLinePlaceholder":58},[33,1152,1153],{"class":35,"line":315},[33,1154,1155],{"class":39},"\u002F\u002F CORRECT: key by stable identity\n",[33,1157,1158,1160,1163,1165,1167,1169,1171],{"class":35,"line":342},[33,1159,181],{"class":180},[33,1161,1162],{"class":184}," EditableTodoListGood",[33,1164,606],{"class":188},[33,1166,609],{"class":192},[33,1168,196],{"class":188},[33,1170,913],{"class":192},[33,1172,612],{"class":188},[33,1174,1175,1177],{"class":35,"line":348},[33,1176,617],{"class":180},[33,1178,620],{"class":188},[33,1180,1181,1183,1185],{"class":35,"line":371},[33,1182,625],{"class":188},[33,1184,629],{"class":628},[33,1186,632],{"class":188},[33,1188,1189,1191,1193,1195,1197,1199],{"class":35,"line":383},[33,1190,637],{"class":188},[33,1192,250],{"class":184},[33,1194,189],{"class":188},[33,1196,644],{"class":192},[33,1198,258],{"class":180},[33,1200,620],{"class":188},[33,1202,1203,1205,1207,1209,1211],{"class":35,"line":395},[33,1204,810],{"class":188},[33,1206,652],{"class":628},[33,1208,815],{"class":184},[33,1210,818],{"class":180},[33,1212,1213],{"class":188},"{todo.id}>\n",[33,1215,1216,1218,1220,1222,1224,1226,1228,1230],{"class":35,"line":401},[33,1217,981],{"class":188},[33,1219,984],{"class":628},[33,1221,987],{"class":184},[33,1223,818],{"class":180},[33,1225,992],{"class":567},[33,1227,995],{"class":184},[33,1229,818],{"class":180},[33,1231,1000],{"class":188},[33,1233,1234,1236,1238,1240,1242],{"class":35,"line":412},[33,1235,981],{"class":188},[33,1237,33],{"class":628},[33,1239,655],{"class":188},[33,1241,33],{"class":628},[33,1243,632],{"class":188},[33,1245,1246,1248,1250,1252,1254,1256,1258,1260,1262,1264],{"class":35,"line":418},[33,1247,981],{"class":188},[33,1249,1019],{"class":628},[33,1251,1022],{"class":184},[33,1253,818],{"class":180},[33,1255,1027],{"class":188},[33,1257,951],{"class":180},[33,1259,1032],{"class":184},[33,1261,1035],{"class":188},[33,1263,1019],{"class":628},[33,1265,632],{"class":188},[33,1267,1268,1270,1272],{"class":35,"line":424},[33,1269,1044],{"class":188},[33,1271,652],{"class":628},[33,1273,632],{"class":188},[33,1275,1276],{"class":35,"line":429},[33,1277,830],{"class":188},[33,1279,1280,1282,1284],{"class":35,"line":435},[33,1281,675],{"class":188},[33,1283,629],{"class":628},[33,1285,632],{"class":188},[33,1287,1288],{"class":35,"line":459},[33,1289,684],{"class":188},[33,1291,1292],{"class":35,"line":471},[33,1293,541],{"class":188},[33,1295,1296],{"class":35,"line":476},[33,1297,1298],{"class":39},"\u002F\u002F Deleting Task A (id:1):\n",[33,1300,1301],{"class":35,"line":481},[33,1302,1303],{"class":39},"\u002F\u002F   React sees key \"1\" is gone → removes that \u003Cli> entirely (including its checkbox).\n",[33,1305,1306],{"class":35,"line":487},[33,1307,1308],{"class":39},"\u002F\u002F   Key \"2\" is still present → its \u003Cli> + checkbox are untouched.\n",[33,1310,1311],{"class":35,"line":504},[33,1312,1313],{"class":39},"\u002F\u002F   Checkbox state stays correctly paired with its todo. ✓\n",[14,1315,1317],{"id":1316},"when-index-as-key-is-actually-safe","When Index-as-Key Is Actually Safe",[19,1319,1321],{"filename":1320,"language":22},"index_key_safe.js",[24,1322,1324],{"className":26,"code":1323,"language":22,"meta":28,"style":28},"\u002F\u002F Index-as-key is safe when ALL of these hold:\n\u002F\u002F 1. The list is never reordered (no sort, no drag-and-drop)\n\u002F\u002F 2. Items are never inserted\u002Fremoved except at the END\n\u002F\u002F 3. List items have NO per-item state (no uncontrolled inputs, no local useState,\n\u002F\u002F    no CSS transitions keyed to identity)\n\n\u002F\u002F SAFE: static navigation — never changes, no per-item state\nconst NAV_ITEMS = ['Home', 'About', 'Products', 'Contact']\n\nfunction NavBar() {\n  return (\n    \u003Cnav>\n      {NAV_ITEMS.map((label, index) => (\n        \u003Ca key={index} href={`\u002F${label.toLowerCase()}`}>{label}\u003C\u002Fa>\n      ))}\n    \u003C\u002Fnav>\n  )\n}\n\n\u002F\u002F SAFE: rendering a fixed set of columns from a static schema\nfunction DataTable({ row, columns }) {\n  return (\n    \u003Ctr>\n      {columns.map((col, index) => (\n        \u003Ctd key={index}>{row[col.field]}\u003C\u002Ftd>\n      ))}\n    \u003C\u002Ftr>\n  )\n  \u002F\u002F columns is a fixed schema that never reorders — index is stable.\n}\n\n\u002F\u002F NEVER SAFE: any list that can be filtered, sorted, prepended, or\n\u002F\u002F have items removed from anywhere but the end.\n",[30,1325,1326,1331,1336,1341,1346,1351,1355,1360,1393,1397,1407,1413,1422,1449,1494,1498,1506,1510,1514,1518,1523,1542,1548,1557,1579,1597,1601,1609,1613,1618,1622,1626,1631],{"__ignoreMap":28},[33,1327,1328],{"class":35,"line":36},[33,1329,1330],{"class":39},"\u002F\u002F Index-as-key is safe when ALL of these hold:\n",[33,1332,1333],{"class":35,"line":43},[33,1334,1335],{"class":39},"\u002F\u002F 1. The list is never reordered (no sort, no drag-and-drop)\n",[33,1337,1338],{"class":35,"line":49},[33,1339,1340],{"class":39},"\u002F\u002F 2. Items are never inserted\u002Fremoved except at the END\n",[33,1342,1343],{"class":35,"line":55},[33,1344,1345],{"class":39},"\u002F\u002F 3. List items have NO per-item state (no uncontrolled inputs, no local useState,\n",[33,1347,1348],{"class":35,"line":62},[33,1349,1350],{"class":39},"\u002F\u002F    no CSS transitions keyed to identity)\n",[33,1352,1353],{"class":35,"line":68},[33,1354,59],{"emptyLinePlaceholder":58},[33,1356,1357],{"class":35,"line":74},[33,1358,1359],{"class":39},"\u002F\u002F SAFE: static navigation — never changes, no per-item state\n",[33,1361,1362,1364,1367,1369,1372,1375,1377,1380,1382,1385,1387,1390],{"class":35,"line":79},[33,1363,303],{"class":180},[33,1365,1366],{"class":234}," NAV_ITEMS",[33,1368,238],{"class":180},[33,1370,1371],{"class":188}," [",[33,1373,1374],{"class":567},"'Home'",[33,1376,196],{"class":188},[33,1378,1379],{"class":567},"'About'",[33,1381,196],{"class":188},[33,1383,1384],{"class":567},"'Products'",[33,1386,196],{"class":188},[33,1388,1389],{"class":567},"'Contact'",[33,1391,1392],{"class":188},"]\n",[33,1394,1395],{"class":35,"line":85},[33,1396,59],{"emptyLinePlaceholder":58},[33,1398,1399,1401,1404],{"class":35,"line":91},[33,1400,181],{"class":180},[33,1402,1403],{"class":184}," NavBar",[33,1405,1406],{"class":188},"() {\n",[33,1408,1409,1411],{"class":35,"line":97},[33,1410,617],{"class":180},[33,1412,620],{"class":188},[33,1414,1415,1417,1420],{"class":35,"line":103},[33,1416,625],{"class":188},[33,1418,1419],{"class":628},"nav",[33,1421,632],{"class":188},[33,1423,1424,1426,1429,1432,1434,1436,1439,1441,1443,1445,1447],{"class":35,"line":109},[33,1425,665],{"class":188},[33,1427,1428],{"class":234},"NAV_ITEMS",[33,1430,1431],{"class":188},".",[33,1433,250],{"class":184},[33,1435,938],{"class":188},[33,1437,1438],{"class":192},"label",[33,1440,196],{"class":188},[33,1442,945],{"class":192},[33,1444,948],{"class":188},[33,1446,951],{"class":180},[33,1448,620],{"class":188},[33,1450,1451,1453,1456,1458,1460,1463,1466,1468,1471,1474,1476,1478,1481,1484,1487,1490,1492],{"class":35,"line":114},[33,1452,810],{"class":188},[33,1454,1455],{"class":628},"a",[33,1457,815],{"class":184},[33,1459,818],{"class":180},[33,1461,1462],{"class":188},"{index} ",[33,1464,1465],{"class":184},"href",[33,1467,818],{"class":180},[33,1469,1470],{"class":188},"{",[33,1472,1473],{"class":567},"`\u002F${",[33,1475,1438],{"class":188},[33,1477,1431],{"class":567},[33,1479,1480],{"class":184},"toLowerCase",[33,1482,1483],{"class":567},"()",[33,1485,1486],{"class":567},"}`",[33,1488,1489],{"class":188},"}>{label}\u003C\u002F",[33,1491,1455],{"class":628},[33,1493,632],{"class":188},[33,1495,1496],{"class":35,"line":120},[33,1497,830],{"class":188},[33,1499,1500,1502,1504],{"class":35,"line":126},[33,1501,675],{"class":188},[33,1503,1419],{"class":628},[33,1505,632],{"class":188},[33,1507,1508],{"class":35,"line":132},[33,1509,684],{"class":188},[33,1511,1512],{"class":35,"line":138},[33,1513,541],{"class":188},[33,1515,1516],{"class":35,"line":143},[33,1517,59],{"emptyLinePlaceholder":58},[33,1519,1520],{"class":35,"line":149},[33,1521,1522],{"class":39},"\u002F\u002F SAFE: rendering a fixed set of columns from a static schema\n",[33,1524,1525,1527,1530,1532,1535,1537,1540],{"class":35,"line":155},[33,1526,181],{"class":180},[33,1528,1529],{"class":184}," DataTable",[33,1531,606],{"class":188},[33,1533,1534],{"class":192},"row",[33,1536,196],{"class":188},[33,1538,1539],{"class":192},"columns",[33,1541,612],{"class":188},[33,1543,1544,1546],{"class":35,"line":161},[33,1545,617],{"class":180},[33,1547,620],{"class":188},[33,1549,1550,1552,1555],{"class":35,"line":166},[33,1551,625],{"class":188},[33,1553,1554],{"class":628},"tr",[33,1556,632],{"class":188},[33,1558,1559,1562,1564,1566,1569,1571,1573,1575,1577],{"class":35,"line":172},[33,1560,1561],{"class":188},"      {columns.",[33,1563,250],{"class":184},[33,1565,938],{"class":188},[33,1567,1568],{"class":192},"col",[33,1570,196],{"class":188},[33,1572,945],{"class":192},[33,1574,948],{"class":188},[33,1576,951],{"class":180},[33,1578,620],{"class":188},[33,1580,1581,1583,1586,1588,1590,1593,1595],{"class":35,"line":177},[33,1582,810],{"class":188},[33,1584,1585],{"class":628},"td",[33,1587,815],{"class":184},[33,1589,818],{"class":180},[33,1591,1592],{"class":188},"{index}>{row[col.field]}\u003C\u002F",[33,1594,1585],{"class":628},[33,1596,632],{"class":188},[33,1598,1599],{"class":35,"line":205},[33,1600,830],{"class":188},[33,1602,1603,1605,1607],{"class":35,"line":211},[33,1604,675],{"class":188},[33,1606,1554],{"class":628},[33,1608,632],{"class":188},[33,1610,1611],{"class":35,"line":217},[33,1612,684],{"class":188},[33,1614,1615],{"class":35,"line":222},[33,1616,1617],{"class":39},"  \u002F\u002F columns is a fixed schema that never reorders — index is stable.\n",[33,1619,1620],{"class":35,"line":228},[33,1621,541],{"class":188},[33,1623,1624],{"class":35,"line":264},[33,1625,59],{"emptyLinePlaceholder":58},[33,1627,1628],{"class":35,"line":277},[33,1629,1630],{"class":39},"\u002F\u002F NEVER SAFE: any list that can be filtered, sorted, prepended, or\n",[33,1632,1633],{"class":35,"line":289},[33,1634,1635],{"class":39},"\u002F\u002F have items removed from anywhere but the end.\n",[14,1637,1639],{"id":1638},"key-requirements-stable-unique-among-siblings","Key Requirements: Stable, Unique Among Siblings",[19,1641,1643],{"filename":1642,"language":22},"key_requirements.js",[24,1644,1646],{"className":26,"code":1645,"language":22,"meta":28,"style":28},"import { Fragment } from 'react'\n\n\u002F\u002F Keys must be:\n\u002F\u002F 1. STABLE across renders — the same item must get the same key every render\n\u002F\u002F 2. UNIQUE among siblings in the same array — NOT globally unique\n\u002F\u002F 3. From the data itself (database id, UUID) — not derived at render time\n\n\u002F\u002F ── Sibling uniqueness, not global ──\nfunction Dashboard({ recentOrders, recentUsers }) {\n  return (\n    \u003C>\n      \u003Cul>\n        {recentOrders.map(o => \u003Cli key={o.id}>Order #{o.id}\u003C\u002Fli>)}\n      \u003C\u002Ful>\n      \u003Cul>\n        {recentUsers.map(u => \u003Cli key={u.id}>User #{u.id}\u003C\u002Fli>)}\n      \u003C\u002Ful>\n      {\u002F* order.id === 7 and user.id === 7 can coexist — different lists,\n          independently keyed. No conflict. *\u002F}\n    \u003C\u002F>\n  )\n}\n\n\u002F\u002F ── ANTI-PATTERN: Math.random() or Date.now() as key ──\nfunction BrokenList({ items }) {\n  return (\n    \u003Cul>\n      {items.map(item => (\n        \u003Cli key={Math.random()}>{item.text}\u003C\u002Fli>\n      ))}\n    \u003C\u002Ful>\n  )\n  \u002F\u002F Every render generates entirely new keys.\n  \u002F\u002F React thinks EVERY item is brand new on EVERY render.\n  \u002F\u002F → Full unmount + remount of the entire list every single render.\n  \u002F\u002F → All per-item state destroyed (focus, scroll position, form input, transitions).\n  \u002F\u002F → Actively WORSE than no key at all.\n}\n\n\u002F\u002F ── ANTI-PATTERN: index in the key string with sortable data ──\nfunction UnstableKeyList({ items }) {\n  return (\n    \u003Cul>\n      {items.map((item, index) => (\n        \u003Cli key={`${item.name}-${index}`}>{item.name}\u003C\u002Fli>\n      ))}\n    \u003C\u002Ful>\n  )\n  \u002F\u002F The index component makes this key unstable on reorder — same as bare index.\n  \u002F\u002F If two items share a name, the key isn't even unique. Use item.id only.\n}\n\n\u002F\u002F ── CORRECT: key on the outermost element produced by .map() ──\nfunction UserTable({ users }) {\n  return (\n    \u003Ctable>\u003Ctbody>\n      {users.map(user => (\n        \u003CUserRow key={user.id} user={user} \u002F>\n        \u002F\u002F key goes on the element directly produced by .map() — UserRow.\n      ))}\n    \u003C\u002Ftbody>\u003C\u002Ftable>\n  )\n}\n\n\u002F\u002F ANTI-PATTERN: key on a nested element — does nothing for the list\nfunction UserRowBad({ user }) {\n  return (\n    \u003Ctr>\n      \u003Ctd key={user.id}>{user.name}\u003C\u002Ftd>\n      {\u002F* WRONG: key here does nothing for reconciliation — this \u003Ctr> is unkeyed. *\u002F}\n    \u003C\u002Ftr>\n  )\n}\n\n\u002F\u002F ── Fragments with keys: multiple sibling elements per list item ──\nfunction DefinitionList({ terms }) {\n  return (\n    \u003Cdl>\n      {terms.map(term => (\n        \u002F\u002F The shorthand \u003C>...\u003C\u002F> CANNOT accept a key prop — syntax error.\n        \u002F\u002F Must use the explicit \u003CFragment key={...}> form.\n        \u003CFragment key={term.id}>\n          \u003Cdt>{term.word}\u003C\u002Fdt>\n          \u003Cdd>{term.definition}\u003C\u002Fdd>\n        \u003C\u002FFragment>\n      ))}\n    \u003C\u002Fdl>\n  )\n}\n",[30,1647,1648,1659,1663,1668,1673,1678,1683,1687,1692,1711,1717,1722,1731,1760,1769,1777,1806,1814,1821,1828,1833,1837,1841,1845,1850,1864,1870,1878,1894,1917,1921,1929,1933,1938,1943,1948,1953,1958,1962,1966,1971,1984,1990,1998,2018,2054,2058,2066,2070,2075,2080,2084,2088,2093,2107,2113,2128,2144,2166,2172,2177,2191,2196,2201,2206,2212,2226,2233,2242,2260,2270,2279,2284,2289,2294,2300,2315,2322,2332,2349,2355,2361,2376,2391,2406,2415,2420,2429,2434],{"__ignoreMap":28},[33,1649,1650,1652,1655,1657],{"class":35,"line":36},[33,1651,558],{"class":180},[33,1653,1654],{"class":188}," { Fragment } ",[33,1656,564],{"class":180},[33,1658,568],{"class":567},[33,1660,1661],{"class":35,"line":43},[33,1662,59],{"emptyLinePlaceholder":58},[33,1664,1665],{"class":35,"line":49},[33,1666,1667],{"class":39},"\u002F\u002F Keys must be:\n",[33,1669,1670],{"class":35,"line":55},[33,1671,1672],{"class":39},"\u002F\u002F 1. STABLE across renders — the same item must get the same key every render\n",[33,1674,1675],{"class":35,"line":62},[33,1676,1677],{"class":39},"\u002F\u002F 2. UNIQUE among siblings in the same array — NOT globally unique\n",[33,1679,1680],{"class":35,"line":68},[33,1681,1682],{"class":39},"\u002F\u002F 3. From the data itself (database id, UUID) — not derived at render time\n",[33,1684,1685],{"class":35,"line":74},[33,1686,59],{"emptyLinePlaceholder":58},[33,1688,1689],{"class":35,"line":79},[33,1690,1691],{"class":39},"\u002F\u002F ── Sibling uniqueness, not global ──\n",[33,1693,1694,1696,1699,1701,1704,1706,1709],{"class":35,"line":85},[33,1695,181],{"class":180},[33,1697,1698],{"class":184}," Dashboard",[33,1700,606],{"class":188},[33,1702,1703],{"class":192},"recentOrders",[33,1705,196],{"class":188},[33,1707,1708],{"class":192},"recentUsers",[33,1710,612],{"class":188},[33,1712,1713,1715],{"class":35,"line":91},[33,1714,617],{"class":180},[33,1716,620],{"class":188},[33,1718,1719],{"class":35,"line":97},[33,1720,1721],{"class":188},"    \u003C>\n",[33,1723,1724,1727,1729],{"class":35,"line":103},[33,1725,1726],{"class":188},"      \u003C",[33,1728,629],{"class":628},[33,1730,632],{"class":188},[33,1732,1733,1736,1738,1740,1743,1745,1747,1749,1751,1753,1756,1758],{"class":35,"line":109},[33,1734,1735],{"class":188},"        {recentOrders.",[33,1737,250],{"class":184},[33,1739,189],{"class":188},[33,1741,1742],{"class":192},"o",[33,1744,258],{"class":180},[33,1746,649],{"class":188},[33,1748,652],{"class":628},[33,1750,815],{"class":184},[33,1752,818],{"class":180},[33,1754,1755],{"class":188},"{o.id}>Order #{o.id}\u003C\u002F",[33,1757,652],{"class":628},[33,1759,660],{"class":188},[33,1761,1762,1765,1767],{"class":35,"line":114},[33,1763,1764],{"class":188},"      \u003C\u002F",[33,1766,629],{"class":628},[33,1768,632],{"class":188},[33,1770,1771,1773,1775],{"class":35,"line":120},[33,1772,1726],{"class":188},[33,1774,629],{"class":628},[33,1776,632],{"class":188},[33,1778,1779,1782,1784,1786,1789,1791,1793,1795,1797,1799,1802,1804],{"class":35,"line":126},[33,1780,1781],{"class":188},"        {recentUsers.",[33,1783,250],{"class":184},[33,1785,189],{"class":188},[33,1787,1788],{"class":192},"u",[33,1790,258],{"class":180},[33,1792,649],{"class":188},[33,1794,652],{"class":628},[33,1796,815],{"class":184},[33,1798,818],{"class":180},[33,1800,1801],{"class":188},"{u.id}>User #{u.id}\u003C\u002F",[33,1803,652],{"class":628},[33,1805,660],{"class":188},[33,1807,1808,1810,1812],{"class":35,"line":132},[33,1809,1764],{"class":188},[33,1811,629],{"class":628},[33,1813,632],{"class":188},[33,1815,1816,1818],{"class":35,"line":138},[33,1817,665],{"class":188},[33,1819,1820],{"class":39},"\u002F* order.id === 7 and user.id === 7 can coexist — different lists,\n",[33,1822,1823,1826],{"class":35,"line":143},[33,1824,1825],{"class":39},"          independently keyed. No conflict. *\u002F",[33,1827,541],{"class":188},[33,1829,1830],{"class":35,"line":149},[33,1831,1832],{"class":188},"    \u003C\u002F>\n",[33,1834,1835],{"class":35,"line":155},[33,1836,684],{"class":188},[33,1838,1839],{"class":35,"line":161},[33,1840,541],{"class":188},[33,1842,1843],{"class":35,"line":166},[33,1844,59],{"emptyLinePlaceholder":58},[33,1846,1847],{"class":35,"line":172},[33,1848,1849],{"class":39},"\u002F\u002F ── ANTI-PATTERN: Math.random() or Date.now() as key ──\n",[33,1851,1852,1854,1857,1859,1862],{"class":35,"line":177},[33,1853,181],{"class":180},[33,1855,1856],{"class":184}," BrokenList",[33,1858,606],{"class":188},[33,1860,1861],{"class":192},"items",[33,1863,612],{"class":188},[33,1865,1866,1868],{"class":35,"line":205},[33,1867,617],{"class":180},[33,1869,620],{"class":188},[33,1871,1872,1874,1876],{"class":35,"line":211},[33,1873,625],{"class":188},[33,1875,629],{"class":628},[33,1877,632],{"class":188},[33,1879,1880,1883,1885,1887,1890,1892],{"class":35,"line":217},[33,1881,1882],{"class":188},"      {items.",[33,1884,250],{"class":184},[33,1886,189],{"class":188},[33,1888,1889],{"class":192},"item",[33,1891,258],{"class":180},[33,1893,620],{"class":188},[33,1895,1896,1898,1900,1902,1904,1907,1910,1913,1915],{"class":35,"line":222},[33,1897,810],{"class":188},[33,1899,652],{"class":628},[33,1901,815],{"class":184},[33,1903,818],{"class":180},[33,1905,1906],{"class":188},"{Math.",[33,1908,1909],{"class":184},"random",[33,1911,1912],{"class":188},"()}>{item.text}\u003C\u002F",[33,1914,652],{"class":628},[33,1916,632],{"class":188},[33,1918,1919],{"class":35,"line":228},[33,1920,830],{"class":188},[33,1922,1923,1925,1927],{"class":35,"line":264},[33,1924,675],{"class":188},[33,1926,629],{"class":628},[33,1928,632],{"class":188},[33,1930,1931],{"class":35,"line":277},[33,1932,684],{"class":188},[33,1934,1935],{"class":35,"line":289},[33,1936,1937],{"class":39},"  \u002F\u002F Every render generates entirely new keys.\n",[33,1939,1940],{"class":35,"line":294},[33,1941,1942],{"class":39},"  \u002F\u002F React thinks EVERY item is brand new on EVERY render.\n",[33,1944,1945],{"class":35,"line":315},[33,1946,1947],{"class":39},"  \u002F\u002F → Full unmount + remount of the entire list every single render.\n",[33,1949,1950],{"class":35,"line":342},[33,1951,1952],{"class":39},"  \u002F\u002F → All per-item state destroyed (focus, scroll position, form input, transitions).\n",[33,1954,1955],{"class":35,"line":348},[33,1956,1957],{"class":39},"  \u002F\u002F → Actively WORSE than no key at all.\n",[33,1959,1960],{"class":35,"line":371},[33,1961,541],{"class":188},[33,1963,1964],{"class":35,"line":383},[33,1965,59],{"emptyLinePlaceholder":58},[33,1967,1968],{"class":35,"line":395},[33,1969,1970],{"class":39},"\u002F\u002F ── ANTI-PATTERN: index in the key string with sortable data ──\n",[33,1972,1973,1975,1978,1980,1982],{"class":35,"line":401},[33,1974,181],{"class":180},[33,1976,1977],{"class":184}," UnstableKeyList",[33,1979,606],{"class":188},[33,1981,1861],{"class":192},[33,1983,612],{"class":188},[33,1985,1986,1988],{"class":35,"line":412},[33,1987,617],{"class":180},[33,1989,620],{"class":188},[33,1991,1992,1994,1996],{"class":35,"line":418},[33,1993,625],{"class":188},[33,1995,629],{"class":628},[33,1997,632],{"class":188},[33,1999,2000,2002,2004,2006,2008,2010,2012,2014,2016],{"class":35,"line":424},[33,2001,1882],{"class":188},[33,2003,250],{"class":184},[33,2005,938],{"class":188},[33,2007,1889],{"class":192},[33,2009,196],{"class":188},[33,2011,945],{"class":192},[33,2013,948],{"class":188},[33,2015,951],{"class":180},[33,2017,620],{"class":188},[33,2019,2020,2022,2024,2026,2028,2030,2033,2035,2037,2040,2043,2045,2047,2050,2052],{"class":35,"line":429},[33,2021,810],{"class":188},[33,2023,652],{"class":628},[33,2025,815],{"class":184},[33,2027,818],{"class":180},[33,2029,1470],{"class":188},[33,2031,2032],{"class":567},"`${",[33,2034,1889],{"class":188},[33,2036,1431],{"class":567},[33,2038,2039],{"class":188},"name",[33,2041,2042],{"class":567},"}-${",[33,2044,945],{"class":188},[33,2046,1486],{"class":567},[33,2048,2049],{"class":188},"}>{item.name}\u003C\u002F",[33,2051,652],{"class":628},[33,2053,632],{"class":188},[33,2055,2056],{"class":35,"line":435},[33,2057,830],{"class":188},[33,2059,2060,2062,2064],{"class":35,"line":459},[33,2061,675],{"class":188},[33,2063,629],{"class":628},[33,2065,632],{"class":188},[33,2067,2068],{"class":35,"line":471},[33,2069,684],{"class":188},[33,2071,2072],{"class":35,"line":476},[33,2073,2074],{"class":39},"  \u002F\u002F The index component makes this key unstable on reorder — same as bare index.\n",[33,2076,2077],{"class":35,"line":481},[33,2078,2079],{"class":39},"  \u002F\u002F If two items share a name, the key isn't even unique. Use item.id only.\n",[33,2081,2082],{"class":35,"line":487},[33,2083,541],{"class":188},[33,2085,2086],{"class":35,"line":504},[33,2087,59],{"emptyLinePlaceholder":58},[33,2089,2090],{"class":35,"line":513},[33,2091,2092],{"class":39},"\u002F\u002F ── CORRECT: key on the outermost element produced by .map() ──\n",[33,2094,2095,2097,2100,2102,2105],{"class":35,"line":518},[33,2096,181],{"class":180},[33,2098,2099],{"class":184}," UserTable",[33,2101,606],{"class":188},[33,2103,2104],{"class":192},"users",[33,2106,612],{"class":188},[33,2108,2109,2111],{"class":35,"line":523},[33,2110,617],{"class":180},[33,2112,620],{"class":188},[33,2114,2115,2117,2120,2123,2126],{"class":35,"line":529},[33,2116,625],{"class":188},[33,2118,2119],{"class":628},"table",[33,2121,2122],{"class":188},">\u003C",[33,2124,2125],{"class":628},"tbody",[33,2127,632],{"class":188},[33,2129,2130,2133,2135,2137,2140,2142],{"class":35,"line":538},[33,2131,2132],{"class":188},"      {users.",[33,2134,250],{"class":184},[33,2136,189],{"class":188},[33,2138,2139],{"class":192},"user",[33,2141,258],{"class":180},[33,2143,620],{"class":188},[33,2145,2147,2149,2152,2154,2156,2159,2161,2163],{"class":35,"line":2146},58,[33,2148,810],{"class":188},[33,2150,2151],{"class":234},"UserRow",[33,2153,815],{"class":184},[33,2155,818],{"class":180},[33,2157,2158],{"class":188},"{user.id} ",[33,2160,2139],{"class":184},[33,2162,818],{"class":180},[33,2164,2165],{"class":188},"{user} \u002F>\n",[33,2167,2169],{"class":35,"line":2168},59,[33,2170,2171],{"class":39},"        \u002F\u002F key goes on the element directly produced by .map() — UserRow.\n",[33,2173,2175],{"class":35,"line":2174},60,[33,2176,830],{"class":188},[33,2178,2180,2182,2184,2187,2189],{"class":35,"line":2179},61,[33,2181,675],{"class":188},[33,2183,2125],{"class":628},[33,2185,2186],{"class":188},">\u003C\u002F",[33,2188,2119],{"class":628},[33,2190,632],{"class":188},[33,2192,2194],{"class":35,"line":2193},62,[33,2195,684],{"class":188},[33,2197,2199],{"class":35,"line":2198},63,[33,2200,541],{"class":188},[33,2202,2204],{"class":35,"line":2203},64,[33,2205,59],{"emptyLinePlaceholder":58},[33,2207,2209],{"class":35,"line":2208},65,[33,2210,2211],{"class":39},"\u002F\u002F ANTI-PATTERN: key on a nested element — does nothing for the list\n",[33,2213,2215,2217,2220,2222,2224],{"class":35,"line":2214},66,[33,2216,181],{"class":180},[33,2218,2219],{"class":184}," UserRowBad",[33,2221,606],{"class":188},[33,2223,2139],{"class":192},[33,2225,612],{"class":188},[33,2227,2229,2231],{"class":35,"line":2228},67,[33,2230,617],{"class":180},[33,2232,620],{"class":188},[33,2234,2236,2238,2240],{"class":35,"line":2235},68,[33,2237,625],{"class":188},[33,2239,1554],{"class":628},[33,2241,632],{"class":188},[33,2243,2245,2247,2249,2251,2253,2256,2258],{"class":35,"line":2244},69,[33,2246,1726],{"class":188},[33,2248,1585],{"class":628},[33,2250,815],{"class":184},[33,2252,818],{"class":180},[33,2254,2255],{"class":188},"{user.id}>{user.name}\u003C\u002F",[33,2257,1585],{"class":628},[33,2259,632],{"class":188},[33,2261,2263,2265,2268],{"class":35,"line":2262},70,[33,2264,665],{"class":188},[33,2266,2267],{"class":39},"\u002F* WRONG: key here does nothing for reconciliation — this \u003Ctr> is unkeyed. *\u002F",[33,2269,541],{"class":188},[33,2271,2273,2275,2277],{"class":35,"line":2272},71,[33,2274,675],{"class":188},[33,2276,1554],{"class":628},[33,2278,632],{"class":188},[33,2280,2282],{"class":35,"line":2281},72,[33,2283,684],{"class":188},[33,2285,2287],{"class":35,"line":2286},73,[33,2288,541],{"class":188},[33,2290,2292],{"class":35,"line":2291},74,[33,2293,59],{"emptyLinePlaceholder":58},[33,2295,2297],{"class":35,"line":2296},75,[33,2298,2299],{"class":39},"\u002F\u002F ── Fragments with keys: multiple sibling elements per list item ──\n",[33,2301,2303,2305,2308,2310,2313],{"class":35,"line":2302},76,[33,2304,181],{"class":180},[33,2306,2307],{"class":184}," DefinitionList",[33,2309,606],{"class":188},[33,2311,2312],{"class":192},"terms",[33,2314,612],{"class":188},[33,2316,2318,2320],{"class":35,"line":2317},77,[33,2319,617],{"class":180},[33,2321,620],{"class":188},[33,2323,2325,2327,2330],{"class":35,"line":2324},78,[33,2326,625],{"class":188},[33,2328,2329],{"class":628},"dl",[33,2331,632],{"class":188},[33,2333,2335,2338,2340,2342,2345,2347],{"class":35,"line":2334},79,[33,2336,2337],{"class":188},"      {terms.",[33,2339,250],{"class":184},[33,2341,189],{"class":188},[33,2343,2344],{"class":192},"term",[33,2346,258],{"class":180},[33,2348,620],{"class":188},[33,2350,2352],{"class":35,"line":2351},80,[33,2353,2354],{"class":39},"        \u002F\u002F The shorthand \u003C>...\u003C\u002F> CANNOT accept a key prop — syntax error.\n",[33,2356,2358],{"class":35,"line":2357},81,[33,2359,2360],{"class":39},"        \u002F\u002F Must use the explicit \u003CFragment key={...}> form.\n",[33,2362,2364,2366,2369,2371,2373],{"class":35,"line":2363},82,[33,2365,810],{"class":188},[33,2367,2368],{"class":234},"Fragment",[33,2370,815],{"class":184},[33,2372,818],{"class":180},[33,2374,2375],{"class":188},"{term.id}>\n",[33,2377,2379,2381,2384,2387,2389],{"class":35,"line":2378},83,[33,2380,981],{"class":188},[33,2382,2383],{"class":628},"dt",[33,2385,2386],{"class":188},">{term.word}\u003C\u002F",[33,2388,2383],{"class":628},[33,2390,632],{"class":188},[33,2392,2394,2396,2399,2402,2404],{"class":35,"line":2393},84,[33,2395,981],{"class":188},[33,2397,2398],{"class":628},"dd",[33,2400,2401],{"class":188},">{term.definition}\u003C\u002F",[33,2403,2398],{"class":628},[33,2405,632],{"class":188},[33,2407,2409,2411,2413],{"class":35,"line":2408},85,[33,2410,1044],{"class":188},[33,2412,2368],{"class":234},[33,2414,632],{"class":188},[33,2416,2418],{"class":35,"line":2417},86,[33,2419,830],{"class":188},[33,2421,2423,2425,2427],{"class":35,"line":2422},87,[33,2424,675],{"class":188},[33,2426,2329],{"class":628},[33,2428,632],{"class":188},[33,2430,2432],{"class":35,"line":2431},88,[33,2433,684],{"class":188},[33,2435,2437],{"class":35,"line":2436},89,[33,2438,541],{"class":188},[14,2440,2442],{"id":2441},"reordering-bugs-a-deep-dive","Reordering Bugs: A Deep Dive",[19,2444,2446],{"filename":2445,"language":22},"reordering_bugs.js",[24,2447,2449],{"className":26,"code":2448,"language":22,"meta":28,"style":28},"import { useState } from 'react'\n\n\u002F\u002F A list where each item has local state (an expanding\u002Fcollapsing detail panel).\n\u002F\u002F The bug: index keys cause state to \"jump\" to the wrong row after a sort.\n\nfunction SortableListBad({ items }) {\n  const [sortAsc, setSortAsc] = useState(true)\n  const sorted = [...items].sort((a, b) => sortAsc ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name))\n\n  return (\n    \u003Cdiv>\n      \u003Cbutton onClick={() => setSortAsc(s => !s)}>Toggle Sort\u003C\u002Fbutton>\n      \u003Cul>\n        {sorted.map((item, index) => (\n          \u003CExpandableRow key={index} item={item} \u002F>\n        ))}\n      \u003C\u002Ful>\n    \u003C\u002Fdiv>\n  )\n  \u002F\u002F After sorting, React matches by index:\n  \u002F\u002F   old index-0 (was \"Alice\", expanded) → new index-0 (now \"Bob\")\n  \u002F\u002F   React reuses Alice's ExpandableRow instance — including its expanded state.\n  \u002F\u002F   Bob now appears expanded even though the user never expanded Bob.\n  \u002F\u002F   Alice's expanded state has \"jumped\" to Bob's row.\n}\n\nfunction SortableListGood({ items }) {\n  const [sortAsc, setSortAsc] = useState(true)\n  const sorted = [...items].sort((a, b) => sortAsc ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name))\n\n  return (\n    \u003Cdiv>\n      \u003Cbutton onClick={() => setSortAsc(s => !s)}>Toggle Sort\u003C\u002Fbutton>\n      \u003Cul>\n        {sorted.map(item => (\n          \u003CExpandableRow key={item.id} item={item} \u002F>\n        ))}\n      \u003C\u002Ful>\n    \u003C\u002Fdiv>\n  )\n  \u002F\u002F After sorting, React matches by item.id:\n  \u002F\u002F   key \"alice-1\" still exists → Alice's ExpandableRow (with its expanded state)\n  \u002F\u002F   is preserved and moved to its new position in the DOM.\n  \u002F\u002F   key \"bob-2\" still exists → Bob's ExpandableRow (collapsed) moves to its position.\n  \u002F\u002F   No state jumps. Each item's state follows the item, not the position. ✓\n}\n\nfunction ExpandableRow({ item }) {\n  const [expanded, setExpanded] = useState(false)\n  return (\n    \u003Cli>\n      \u003Cbutton onClick={() => setExpanded(e => !e)}>\n        {item.name} {expanded ? '▼' : '▶'}\n      \u003C\u002Fbutton>\n      {expanded && \u003Cdiv className=\"details\">{item.details}\u003C\u002Fdiv>}\n    \u003C\u002Fli>\n  )\n}\n",[30,2450,2451,2461,2465,2470,2475,2479,2492,2521,2580,2584,2590,2599,2633,2641,2662,2682,2687,2695,2703,2707,2712,2717,2722,2727,2732,2736,2740,2753,2777,2823,2827,2833,2841,2871,2879,2893,2912,2916,2924,2932,2936,2941,2946,2951,2956,2961,2965,2969,2982,3009,3015,3023,3052,3070,3078,3106,3114,3118],{"__ignoreMap":28},[33,2452,2453,2455,2457,2459],{"class":35,"line":36},[33,2454,558],{"class":180},[33,2456,561],{"class":188},[33,2458,564],{"class":180},[33,2460,568],{"class":567},[33,2462,2463],{"class":35,"line":43},[33,2464,59],{"emptyLinePlaceholder":58},[33,2466,2467],{"class":35,"line":49},[33,2468,2469],{"class":39},"\u002F\u002F A list where each item has local state (an expanding\u002Fcollapsing detail panel).\n",[33,2471,2472],{"class":35,"line":55},[33,2473,2474],{"class":39},"\u002F\u002F The bug: index keys cause state to \"jump\" to the wrong row after a sort.\n",[33,2476,2477],{"class":35,"line":62},[33,2478,59],{"emptyLinePlaceholder":58},[33,2480,2481,2483,2486,2488,2490],{"class":35,"line":68},[33,2482,181],{"class":180},[33,2484,2485],{"class":184}," SortableListBad",[33,2487,606],{"class":188},[33,2489,1861],{"class":192},[33,2491,612],{"class":188},[33,2493,2494,2496,2498,2501,2503,2506,2508,2510,2513,2515,2518],{"class":35,"line":74},[33,2495,231],{"class":180},[33,2497,1371],{"class":188},[33,2499,2500],{"class":234},"sortAsc",[33,2502,196],{"class":188},[33,2504,2505],{"class":234},"setSortAsc",[33,2507,450],{"class":188},[33,2509,818],{"class":180},[33,2511,2512],{"class":184}," useState",[33,2514,189],{"class":188},[33,2516,2517],{"class":234},"true",[33,2519,2520],{"class":188},")\n",[33,2522,2523,2525,2528,2530,2532,2535,2538,2541,2543,2545,2547,2550,2552,2554,2557,2560,2563,2566,2569,2572,2575,2577],{"class":35,"line":79},[33,2524,231],{"class":180},[33,2526,2527],{"class":234}," sorted",[33,2529,238],{"class":180},[33,2531,1371],{"class":188},[33,2533,2534],{"class":180},"...",[33,2536,2537],{"class":188},"items].",[33,2539,2540],{"class":184},"sort",[33,2542,938],{"class":188},[33,2544,1455],{"class":192},[33,2546,196],{"class":188},[33,2548,2549],{"class":192},"b",[33,2551,948],{"class":188},[33,2553,951],{"class":180},[33,2555,2556],{"class":188}," sortAsc ",[33,2558,2559],{"class":180},"?",[33,2561,2562],{"class":188}," a.name.",[33,2564,2565],{"class":184},"localeCompare",[33,2567,2568],{"class":188},"(b.name) ",[33,2570,2571],{"class":180},":",[33,2573,2574],{"class":188}," b.name.",[33,2576,2565],{"class":184},[33,2578,2579],{"class":188},"(a.name))\n",[33,2581,2582],{"class":35,"line":85},[33,2583,59],{"emptyLinePlaceholder":58},[33,2585,2586,2588],{"class":35,"line":91},[33,2587,617],{"class":180},[33,2589,620],{"class":188},[33,2591,2592,2594,2597],{"class":35,"line":97},[33,2593,625],{"class":188},[33,2595,2596],{"class":628},"div",[33,2598,632],{"class":188},[33,2600,2601,2603,2605,2607,2609,2611,2613,2616,2618,2621,2623,2626,2629,2631],{"class":35,"line":103},[33,2602,1726],{"class":188},[33,2604,1019],{"class":628},[33,2606,1022],{"class":184},[33,2608,818],{"class":180},[33,2610,1027],{"class":188},[33,2612,951],{"class":180},[33,2614,2615],{"class":184}," setSortAsc",[33,2617,189],{"class":188},[33,2619,2620],{"class":192},"s",[33,2622,258],{"class":180},[33,2624,2625],{"class":180}," !",[33,2627,2628],{"class":188},"s)}>Toggle Sort\u003C\u002F",[33,2630,1019],{"class":628},[33,2632,632],{"class":188},[33,2634,2635,2637,2639],{"class":35,"line":109},[33,2636,1726],{"class":188},[33,2638,629],{"class":628},[33,2640,632],{"class":188},[33,2642,2643,2646,2648,2650,2652,2654,2656,2658,2660],{"class":35,"line":114},[33,2644,2645],{"class":188},"        {sorted.",[33,2647,250],{"class":184},[33,2649,938],{"class":188},[33,2651,1889],{"class":192},[33,2653,196],{"class":188},[33,2655,945],{"class":192},[33,2657,948],{"class":188},[33,2659,951],{"class":180},[33,2661,620],{"class":188},[33,2663,2664,2666,2669,2671,2673,2675,2677,2679],{"class":35,"line":120},[33,2665,981],{"class":188},[33,2667,2668],{"class":234},"ExpandableRow",[33,2670,815],{"class":184},[33,2672,818],{"class":180},[33,2674,1462],{"class":188},[33,2676,1889],{"class":184},[33,2678,818],{"class":180},[33,2680,2681],{"class":188},"{item} \u002F>\n",[33,2683,2684],{"class":35,"line":126},[33,2685,2686],{"class":188},"        ))}\n",[33,2688,2689,2691,2693],{"class":35,"line":132},[33,2690,1764],{"class":188},[33,2692,629],{"class":628},[33,2694,632],{"class":188},[33,2696,2697,2699,2701],{"class":35,"line":138},[33,2698,675],{"class":188},[33,2700,2596],{"class":628},[33,2702,632],{"class":188},[33,2704,2705],{"class":35,"line":143},[33,2706,684],{"class":188},[33,2708,2709],{"class":35,"line":149},[33,2710,2711],{"class":39},"  \u002F\u002F After sorting, React matches by index:\n",[33,2713,2714],{"class":35,"line":155},[33,2715,2716],{"class":39},"  \u002F\u002F   old index-0 (was \"Alice\", expanded) → new index-0 (now \"Bob\")\n",[33,2718,2719],{"class":35,"line":161},[33,2720,2721],{"class":39},"  \u002F\u002F   React reuses Alice's ExpandableRow instance — including its expanded state.\n",[33,2723,2724],{"class":35,"line":166},[33,2725,2726],{"class":39},"  \u002F\u002F   Bob now appears expanded even though the user never expanded Bob.\n",[33,2728,2729],{"class":35,"line":172},[33,2730,2731],{"class":39},"  \u002F\u002F   Alice's expanded state has \"jumped\" to Bob's row.\n",[33,2733,2734],{"class":35,"line":177},[33,2735,541],{"class":188},[33,2737,2738],{"class":35,"line":205},[33,2739,59],{"emptyLinePlaceholder":58},[33,2741,2742,2744,2747,2749,2751],{"class":35,"line":211},[33,2743,181],{"class":180},[33,2745,2746],{"class":184}," SortableListGood",[33,2748,606],{"class":188},[33,2750,1861],{"class":192},[33,2752,612],{"class":188},[33,2754,2755,2757,2759,2761,2763,2765,2767,2769,2771,2773,2775],{"class":35,"line":217},[33,2756,231],{"class":180},[33,2758,1371],{"class":188},[33,2760,2500],{"class":234},[33,2762,196],{"class":188},[33,2764,2505],{"class":234},[33,2766,450],{"class":188},[33,2768,818],{"class":180},[33,2770,2512],{"class":184},[33,2772,189],{"class":188},[33,2774,2517],{"class":234},[33,2776,2520],{"class":188},[33,2778,2779,2781,2783,2785,2787,2789,2791,2793,2795,2797,2799,2801,2803,2805,2807,2809,2811,2813,2815,2817,2819,2821],{"class":35,"line":222},[33,2780,231],{"class":180},[33,2782,2527],{"class":234},[33,2784,238],{"class":180},[33,2786,1371],{"class":188},[33,2788,2534],{"class":180},[33,2790,2537],{"class":188},[33,2792,2540],{"class":184},[33,2794,938],{"class":188},[33,2796,1455],{"class":192},[33,2798,196],{"class":188},[33,2800,2549],{"class":192},[33,2802,948],{"class":188},[33,2804,951],{"class":180},[33,2806,2556],{"class":188},[33,2808,2559],{"class":180},[33,2810,2562],{"class":188},[33,2812,2565],{"class":184},[33,2814,2568],{"class":188},[33,2816,2571],{"class":180},[33,2818,2574],{"class":188},[33,2820,2565],{"class":184},[33,2822,2579],{"class":188},[33,2824,2825],{"class":35,"line":228},[33,2826,59],{"emptyLinePlaceholder":58},[33,2828,2829,2831],{"class":35,"line":264},[33,2830,617],{"class":180},[33,2832,620],{"class":188},[33,2834,2835,2837,2839],{"class":35,"line":277},[33,2836,625],{"class":188},[33,2838,2596],{"class":628},[33,2840,632],{"class":188},[33,2842,2843,2845,2847,2849,2851,2853,2855,2857,2859,2861,2863,2865,2867,2869],{"class":35,"line":289},[33,2844,1726],{"class":188},[33,2846,1019],{"class":628},[33,2848,1022],{"class":184},[33,2850,818],{"class":180},[33,2852,1027],{"class":188},[33,2854,951],{"class":180},[33,2856,2615],{"class":184},[33,2858,189],{"class":188},[33,2860,2620],{"class":192},[33,2862,258],{"class":180},[33,2864,2625],{"class":180},[33,2866,2628],{"class":188},[33,2868,1019],{"class":628},[33,2870,632],{"class":188},[33,2872,2873,2875,2877],{"class":35,"line":294},[33,2874,1726],{"class":188},[33,2876,629],{"class":628},[33,2878,632],{"class":188},[33,2880,2881,2883,2885,2887,2889,2891],{"class":35,"line":315},[33,2882,2645],{"class":188},[33,2884,250],{"class":184},[33,2886,189],{"class":188},[33,2888,1889],{"class":192},[33,2890,258],{"class":180},[33,2892,620],{"class":188},[33,2894,2895,2897,2899,2901,2903,2906,2908,2910],{"class":35,"line":342},[33,2896,981],{"class":188},[33,2898,2668],{"class":234},[33,2900,815],{"class":184},[33,2902,818],{"class":180},[33,2904,2905],{"class":188},"{item.id} ",[33,2907,1889],{"class":184},[33,2909,818],{"class":180},[33,2911,2681],{"class":188},[33,2913,2914],{"class":35,"line":348},[33,2915,2686],{"class":188},[33,2917,2918,2920,2922],{"class":35,"line":371},[33,2919,1764],{"class":188},[33,2921,629],{"class":628},[33,2923,632],{"class":188},[33,2925,2926,2928,2930],{"class":35,"line":383},[33,2927,675],{"class":188},[33,2929,2596],{"class":628},[33,2931,632],{"class":188},[33,2933,2934],{"class":35,"line":395},[33,2935,684],{"class":188},[33,2937,2938],{"class":35,"line":401},[33,2939,2940],{"class":39},"  \u002F\u002F After sorting, React matches by item.id:\n",[33,2942,2943],{"class":35,"line":412},[33,2944,2945],{"class":39},"  \u002F\u002F   key \"alice-1\" still exists → Alice's ExpandableRow (with its expanded state)\n",[33,2947,2948],{"class":35,"line":418},[33,2949,2950],{"class":39},"  \u002F\u002F   is preserved and moved to its new position in the DOM.\n",[33,2952,2953],{"class":35,"line":424},[33,2954,2955],{"class":39},"  \u002F\u002F   key \"bob-2\" still exists → Bob's ExpandableRow (collapsed) moves to its position.\n",[33,2957,2958],{"class":35,"line":429},[33,2959,2960],{"class":39},"  \u002F\u002F   No state jumps. Each item's state follows the item, not the position. ✓\n",[33,2962,2963],{"class":35,"line":435},[33,2964,541],{"class":188},[33,2966,2967],{"class":35,"line":459},[33,2968,59],{"emptyLinePlaceholder":58},[33,2970,2971,2973,2976,2978,2980],{"class":35,"line":471},[33,2972,181],{"class":180},[33,2974,2975],{"class":184}," ExpandableRow",[33,2977,606],{"class":188},[33,2979,1889],{"class":192},[33,2981,612],{"class":188},[33,2983,2984,2986,2988,2991,2993,2996,2998,3000,3002,3004,3007],{"class":35,"line":476},[33,2985,231],{"class":180},[33,2987,1371],{"class":188},[33,2989,2990],{"class":234},"expanded",[33,2992,196],{"class":188},[33,2994,2995],{"class":234},"setExpanded",[33,2997,450],{"class":188},[33,2999,818],{"class":180},[33,3001,2512],{"class":184},[33,3003,189],{"class":188},[33,3005,3006],{"class":234},"false",[33,3008,2520],{"class":188},[33,3010,3011,3013],{"class":35,"line":481},[33,3012,617],{"class":180},[33,3014,620],{"class":188},[33,3016,3017,3019,3021],{"class":35,"line":487},[33,3018,625],{"class":188},[33,3020,652],{"class":628},[33,3022,632],{"class":188},[33,3024,3025,3027,3029,3031,3033,3035,3037,3040,3042,3045,3047,3049],{"class":35,"line":504},[33,3026,1726],{"class":188},[33,3028,1019],{"class":628},[33,3030,1022],{"class":184},[33,3032,818],{"class":180},[33,3034,1027],{"class":188},[33,3036,951],{"class":180},[33,3038,3039],{"class":184}," setExpanded",[33,3041,189],{"class":188},[33,3043,3044],{"class":192},"e",[33,3046,258],{"class":180},[33,3048,2625],{"class":180},[33,3050,3051],{"class":188},"e)}>\n",[33,3053,3054,3057,3059,3062,3065,3068],{"class":35,"line":513},[33,3055,3056],{"class":188},"        {item.name} {expanded ",[33,3058,2559],{"class":180},[33,3060,3061],{"class":567}," '▼'",[33,3063,3064],{"class":180}," :",[33,3066,3067],{"class":567}," '▶'",[33,3069,541],{"class":188},[33,3071,3072,3074,3076],{"class":35,"line":518},[33,3073,1764],{"class":188},[33,3075,1019],{"class":628},[33,3077,632],{"class":188},[33,3079,3080,3083,3086,3088,3090,3093,3095,3098,3101,3103],{"class":35,"line":523},[33,3081,3082],{"class":188},"      {expanded ",[33,3084,3085],{"class":180},"&&",[33,3087,649],{"class":188},[33,3089,2596],{"class":628},[33,3091,3092],{"class":184}," className",[33,3094,818],{"class":180},[33,3096,3097],{"class":567},"\"details\"",[33,3099,3100],{"class":188},">{item.details}\u003C\u002F",[33,3102,2596],{"class":628},[33,3104,3105],{"class":188},">}\n",[33,3107,3108,3110,3112],{"class":35,"line":529},[33,3109,675],{"class":188},[33,3111,652],{"class":628},[33,3113,632],{"class":188},[33,3115,3116],{"class":35,"line":538},[33,3117,684],{"class":188},[33,3119,3120],{"class":35,"line":2146},[33,3121,541],{"class":188},[14,3123,3125,3126],{"id":3124},"forcing-a-remount-with-key","Forcing a Remount with ",[30,3127,3128],{},"key",[19,3130,3132],{"filename":3131,"language":22},"force_remount.js",[24,3133,3135],{"className":26,"code":3134,"language":22,"meta":28,"style":28},"import { useState, useEffect } from 'react'\n\n\u002F\u002F Deliberately CHANGING a key forces React to unmount the old instance\n\u002F\u002F and mount a brand-new one — discarding all internal state, refs, and effects.\n\u002F\u002F This is a legitimate technique when a prop change should reset component state.\n\n\u002F\u002F USE CASE: a profile editor that should reset its form when switching users\nfunction ProfileManager({ userId }) {\n  return (\n    \u003CProfileForm key={userId} userId={userId} \u002F>\n    \u002F\u002F Without key: switching userId updates the SAME instance's props.\n    \u002F\u002F   → Any local draft-form state from the PREVIOUS user lingers.\n    \u002F\u002F   → useEffect dependencies fire, but useState values don't reset.\n    \u002F\u002F With key: switching userId changes the key → React unmounts the old\n    \u002F\u002F   ProfileForm entirely and mounts a fresh one → all state reset. ✓\n  )\n}\n\n\u002F\u002F USE CASE: resetting a form without clearing field-by-field\nfunction FormResetter() {\n  const [formKey, setFormKey] = useState(0)\n  return (\n    \u003Cdiv>\n      \u003CComplexForm key={formKey} \u002F>\n      \u003Cbutton onClick={() => setFormKey(k => k + 1)}>Reset Form\u003C\u002Fbutton>\n      {\u002F* Incrementing formKey remounts ComplexForm → all fields, errors,\n          touched states, etc. are reset to initial values in one line. *\u002F}\n    \u003C\u002Fdiv>\n  )\n}\n\n\u002F\u002F ANTI-PATTERN: using key change as a general-purpose \"force update\"\nfunction BadForceUpdate({ data }) {\n  const [renderKey, setRenderKey] = useState(0)\n  \u002F\u002F Don't do this to force a re-render when data prop changes —\n  \u002F\u002F it discards the ENTIRE subtree (DOM, state, effects) unnecessarily.\n  \u002F\u002F If a component isn't updating when props change, the real fix is usually\n  \u002F\u002F a missing\u002Fincorrect dependency array or a stale closure, not a remount.\n}\n",[30,3136,3137,3148,3152,3157,3162,3167,3171,3176,3190,3196,3217,3222,3227,3232,3237,3242,3246,3250,3254,3259,3268,3295,3301,3309,3323,3363,3370,3377,3385,3389,3393,3397,3402,3416,3442,3447,3452,3457,3462],{"__ignoreMap":28},[33,3138,3139,3141,3144,3146],{"class":35,"line":36},[33,3140,558],{"class":180},[33,3142,3143],{"class":188}," { useState, useEffect } ",[33,3145,564],{"class":180},[33,3147,568],{"class":567},[33,3149,3150],{"class":35,"line":43},[33,3151,59],{"emptyLinePlaceholder":58},[33,3153,3154],{"class":35,"line":49},[33,3155,3156],{"class":39},"\u002F\u002F Deliberately CHANGING a key forces React to unmount the old instance\n",[33,3158,3159],{"class":35,"line":55},[33,3160,3161],{"class":39},"\u002F\u002F and mount a brand-new one — discarding all internal state, refs, and effects.\n",[33,3163,3164],{"class":35,"line":62},[33,3165,3166],{"class":39},"\u002F\u002F This is a legitimate technique when a prop change should reset component state.\n",[33,3168,3169],{"class":35,"line":68},[33,3170,59],{"emptyLinePlaceholder":58},[33,3172,3173],{"class":35,"line":74},[33,3174,3175],{"class":39},"\u002F\u002F USE CASE: a profile editor that should reset its form when switching users\n",[33,3177,3178,3180,3183,3185,3188],{"class":35,"line":79},[33,3179,181],{"class":180},[33,3181,3182],{"class":184}," ProfileManager",[33,3184,606],{"class":188},[33,3186,3187],{"class":192},"userId",[33,3189,612],{"class":188},[33,3191,3192,3194],{"class":35,"line":85},[33,3193,617],{"class":180},[33,3195,620],{"class":188},[33,3197,3198,3200,3203,3205,3207,3210,3212,3214],{"class":35,"line":91},[33,3199,625],{"class":188},[33,3201,3202],{"class":234},"ProfileForm",[33,3204,815],{"class":184},[33,3206,818],{"class":180},[33,3208,3209],{"class":188},"{userId} ",[33,3211,3187],{"class":184},[33,3213,818],{"class":180},[33,3215,3216],{"class":188},"{userId} \u002F>\n",[33,3218,3219],{"class":35,"line":97},[33,3220,3221],{"class":39},"    \u002F\u002F Without key: switching userId updates the SAME instance's props.\n",[33,3223,3224],{"class":35,"line":103},[33,3225,3226],{"class":39},"    \u002F\u002F   → Any local draft-form state from the PREVIOUS user lingers.\n",[33,3228,3229],{"class":35,"line":109},[33,3230,3231],{"class":39},"    \u002F\u002F   → useEffect dependencies fire, but useState values don't reset.\n",[33,3233,3234],{"class":35,"line":114},[33,3235,3236],{"class":39},"    \u002F\u002F With key: switching userId changes the key → React unmounts the old\n",[33,3238,3239],{"class":35,"line":120},[33,3240,3241],{"class":39},"    \u002F\u002F   ProfileForm entirely and mounts a fresh one → all state reset. ✓\n",[33,3243,3244],{"class":35,"line":126},[33,3245,684],{"class":188},[33,3247,3248],{"class":35,"line":132},[33,3249,541],{"class":188},[33,3251,3252],{"class":35,"line":138},[33,3253,59],{"emptyLinePlaceholder":58},[33,3255,3256],{"class":35,"line":143},[33,3257,3258],{"class":39},"\u002F\u002F USE CASE: resetting a form without clearing field-by-field\n",[33,3260,3261,3263,3266],{"class":35,"line":149},[33,3262,181],{"class":180},[33,3264,3265],{"class":184}," FormResetter",[33,3267,1406],{"class":188},[33,3269,3270,3272,3274,3277,3279,3282,3284,3286,3288,3290,3293],{"class":35,"line":155},[33,3271,231],{"class":180},[33,3273,1371],{"class":188},[33,3275,3276],{"class":234},"formKey",[33,3278,196],{"class":188},[33,3280,3281],{"class":234},"setFormKey",[33,3283,450],{"class":188},[33,3285,818],{"class":180},[33,3287,2512],{"class":184},[33,3289,189],{"class":188},[33,3291,3292],{"class":234},"0",[33,3294,2520],{"class":188},[33,3296,3297,3299],{"class":35,"line":161},[33,3298,617],{"class":180},[33,3300,620],{"class":188},[33,3302,3303,3305,3307],{"class":35,"line":166},[33,3304,625],{"class":188},[33,3306,2596],{"class":628},[33,3308,632],{"class":188},[33,3310,3311,3313,3316,3318,3320],{"class":35,"line":172},[33,3312,1726],{"class":188},[33,3314,3315],{"class":234},"ComplexForm",[33,3317,815],{"class":184},[33,3319,818],{"class":180},[33,3321,3322],{"class":188},"{formKey} \u002F>\n",[33,3324,3325,3327,3329,3331,3333,3335,3337,3340,3342,3345,3347,3350,3353,3356,3359,3361],{"class":35,"line":177},[33,3326,1726],{"class":188},[33,3328,1019],{"class":628},[33,3330,1022],{"class":184},[33,3332,818],{"class":180},[33,3334,1027],{"class":188},[33,3336,951],{"class":180},[33,3338,3339],{"class":184}," setFormKey",[33,3341,189],{"class":188},[33,3343,3344],{"class":192},"k",[33,3346,258],{"class":180},[33,3348,3349],{"class":188}," k ",[33,3351,3352],{"class":180},"+",[33,3354,3355],{"class":234}," 1",[33,3357,3358],{"class":188},")}>Reset Form\u003C\u002F",[33,3360,1019],{"class":628},[33,3362,632],{"class":188},[33,3364,3365,3367],{"class":35,"line":205},[33,3366,665],{"class":188},[33,3368,3369],{"class":39},"\u002F* Incrementing formKey remounts ComplexForm → all fields, errors,\n",[33,3371,3372,3375],{"class":35,"line":211},[33,3373,3374],{"class":39},"          touched states, etc. are reset to initial values in one line. *\u002F",[33,3376,541],{"class":188},[33,3378,3379,3381,3383],{"class":35,"line":217},[33,3380,675],{"class":188},[33,3382,2596],{"class":628},[33,3384,632],{"class":188},[33,3386,3387],{"class":35,"line":222},[33,3388,684],{"class":188},[33,3390,3391],{"class":35,"line":228},[33,3392,541],{"class":188},[33,3394,3395],{"class":35,"line":264},[33,3396,59],{"emptyLinePlaceholder":58},[33,3398,3399],{"class":35,"line":277},[33,3400,3401],{"class":39},"\u002F\u002F ANTI-PATTERN: using key change as a general-purpose \"force update\"\n",[33,3403,3404,3406,3409,3411,3414],{"class":35,"line":289},[33,3405,181],{"class":180},[33,3407,3408],{"class":184}," BadForceUpdate",[33,3410,606],{"class":188},[33,3412,3413],{"class":192},"data",[33,3415,612],{"class":188},[33,3417,3418,3420,3422,3425,3427,3430,3432,3434,3436,3438,3440],{"class":35,"line":294},[33,3419,231],{"class":180},[33,3421,1371],{"class":188},[33,3423,3424],{"class":234},"renderKey",[33,3426,196],{"class":188},[33,3428,3429],{"class":234},"setRenderKey",[33,3431,450],{"class":188},[33,3433,818],{"class":180},[33,3435,2512],{"class":184},[33,3437,189],{"class":188},[33,3439,3292],{"class":234},[33,3441,2520],{"class":188},[33,3443,3444],{"class":35,"line":315},[33,3445,3446],{"class":39},"  \u002F\u002F Don't do this to force a re-render when data prop changes —\n",[33,3448,3449],{"class":35,"line":342},[33,3450,3451],{"class":39},"  \u002F\u002F it discards the ENTIRE subtree (DOM, state, effects) unnecessarily.\n",[33,3453,3454],{"class":35,"line":348},[33,3455,3456],{"class":39},"  \u002F\u002F If a component isn't updating when props change, the real fix is usually\n",[33,3458,3459],{"class":35,"line":371},[33,3460,3461],{"class":39},"  \u002F\u002F a missing\u002Fincorrect dependency array or a stale closure, not a remount.\n",[33,3463,3464],{"class":35,"line":383},[33,3465,541],{"class":188},[14,3467,3469],{"id":3468},"production-patterns-for-dynamic-lists","Production Patterns for Dynamic Lists",[19,3471,3473],{"filename":3472,"language":22},"production_patterns.js",[24,3474,3476],{"className":26,"code":3475,"language":22,"meta":28,"style":28},"import { useState, useMemo, useCallback, useRef } from 'react'\n\n\u002F\u002F ── 1. Stable key generation when data lacks a natural id ──\nfunction useStableKeys(items) {\n  const keyMapRef = useRef(new Map())\n\n  return useMemo(() => {\n    const keyMap = keyMapRef.current\n    const keys = items.map(item => {\n      \u002F\u002F Reuse existing key if we've seen this item reference before\n      if (keyMap.has(item)) return keyMap.get(item)\n      \u002F\u002F Generate a new stable key for new items\n      const key = crypto.randomUUID ? crypto.randomUUID() : `key-${keyMap.size}`\n      keyMap.set(item, key)\n      return key\n    })\n    \u002F\u002F Clean up keys for items no longer present\n    const currentSet = new Set(items)\n    for (const [item] of keyMap) {\n      if (!currentSet.has(item)) keyMap.delete(item)\n    }\n    return keys\n  }, [items])\n}\n\u002F\u002F Use case: items come from an external source without ids (e.g., parsed CSV rows).\n\u002F\u002F The ref-based map ensures each item reference gets a stable key across renders.\n\n\u002F\u002F ── 2. Virtualized list for large datasets (conceptual) ──\n\u002F\u002F Rendering 10,000 \u003Cli> elements simultaneously will freeze the browser.\n\u002F\u002F Virtualization renders only the visible window + a small overscan buffer.\n\u002F\u002F Libraries: react-window, react-virtual, @tanstack\u002Freact-virtual.\n\nfunction useVirtualizedList({ itemCount, itemHeight, viewportHeight, scrollTop }) {\n  const startIndex = Math.max(0, Math.floor(scrollTop \u002F itemHeight) - 5)  \u002F\u002F 5 items overscan\n  const visibleCount = Math.ceil(viewportHeight \u002F itemHeight) + 10         \u002F\u002F 10 items overscan\n  const endIndex = Math.min(itemCount, startIndex + visibleCount)\n\n  return useMemo(() => ({\n    startIndex,\n    endIndex,\n    visibleItems: Array.from({ length: endIndex - startIndex }, (_, i) => ({\n      index: startIndex + i,\n      offsetTop: (startIndex + i) * itemHeight,\n    })),\n    totalHeight: itemCount * itemHeight,\n  }), [startIndex, endIndex, itemCount, itemHeight])\n}\n\u002F\u002F Keys remain critical in virtualized lists — the visible window changes as the user\n\u002F\u002F scrolls, and stable keys ensure React reuses (not remounts) items that scroll\n\u002F\u002F back into view after scrolling out and back.\n\n\u002F\u002F ── 3. Optimistic list updates (add\u002Fremove before server confirms) ──\nfunction useOptimisticList(fetchItems, createItem, deleteItem) {\n  const [items, setItems] = useState([])\n  const [pendingOps, setPendingOps] = useState(new Set())\n\n  const refresh = useCallback(async () => {\n    setItems(await fetchItems())\n  }, [fetchItems])\n\n  const optimisticAdd = useCallback(async (newItemData) => {\n    const tempId = `temp-${Date.now()}`\n    const optimisticItem = { ...newItemData, id: tempId, _pending: true }\n    setItems(prev => [...prev, optimisticItem])\n    setPendingOps(prev => new Set([...prev, tempId]))\n    try {\n      const realItem = await createItem(newItemData)\n      setItems(prev => prev.map(item => item.id === tempId ? realItem : item))\n    } catch (err) {\n      setItems(prev => prev.filter(item => item.id !== tempId))  \u002F\u002F rollback\n      throw err\n    } finally {\n      setPendingOps(prev => { const next = new Set(prev); next.delete(tempId); return next })\n    }\n  }, [createItem])\n\n  const optimisticRemove = useCallback(async (id) => {\n    const snapshot = items.find(item => item.id === id)\n    setItems(prev => prev.filter(item => item.id !== id))\n    try {\n      await deleteItem(id)\n    } catch (err) {\n      if (snapshot) setItems(prev => { const next = [...prev, snapshot]; next.sort((a,b) => a.id - b.id); return next })\n      throw err\n    }\n  }, [deleteItem, items])\n\n  return { items, pendingOps, refresh, optimisticAdd, optimisticRemove }\n}\n\u002F\u002F The tempId key ensures the optimistic item is uniquely keyed. When the server\n\u002F\u002F responds with a real id, the key changes → React unmounts the temp item and\n\u002F\u002F mounts the real one. This is the one case where a key change on the same logical\n\u002F\u002F item is intentional and correct.\n",[30,3477,3478,3489,3493,3498,3511,3533,3537,3551,3564,3586,3591,3615,3620,3659,3670,3678,3683,3688,3705,3725,3746,3750,3758,3763,3767,3772,3777,3781,3786,3791,3796,3801,3805,3834,3880,3909,3931,3935,3948,3953,3958,3987,3997,4013,4018,4027,4032,4036,4041,4046,4051,4055,4060,4084,4106,4134,4138,4162,4177,4182,4186,4212,4236,4258,4276,4299,4306,4324,4365,4375,4406,4414,4423,4460,4464,4469,4473,4499,4526,4553,4559,4570,4578,4635,4641,4645,4650,4654,4661,4665,4671,4677,4683],{"__ignoreMap":28},[33,3479,3480,3482,3485,3487],{"class":35,"line":36},[33,3481,558],{"class":180},[33,3483,3484],{"class":188}," { useState, useMemo, useCallback, useRef } ",[33,3486,564],{"class":180},[33,3488,568],{"class":567},[33,3490,3491],{"class":35,"line":43},[33,3492,59],{"emptyLinePlaceholder":58},[33,3494,3495],{"class":35,"line":49},[33,3496,3497],{"class":39},"\u002F\u002F ── 1. Stable key generation when data lacks a natural id ──\n",[33,3499,3500,3502,3505,3507,3509],{"class":35,"line":55},[33,3501,181],{"class":180},[33,3503,3504],{"class":184}," useStableKeys",[33,3506,189],{"class":188},[33,3508,1861],{"class":192},[33,3510,202],{"class":188},[33,3512,3513,3515,3518,3520,3523,3525,3528,3530],{"class":35,"line":62},[33,3514,231],{"class":180},[33,3516,3517],{"class":234}," keyMapRef",[33,3519,238],{"class":180},[33,3521,3522],{"class":184}," useRef",[33,3524,189],{"class":188},[33,3526,3527],{"class":180},"new",[33,3529,244],{"class":184},[33,3531,3532],{"class":188},"())\n",[33,3534,3535],{"class":35,"line":68},[33,3536,59],{"emptyLinePlaceholder":58},[33,3538,3539,3541,3544,3547,3549],{"class":35,"line":74},[33,3540,617],{"class":180},[33,3542,3543],{"class":184}," useMemo",[33,3545,3546],{"class":188},"(() ",[33,3548,951],{"class":180},[33,3550,392],{"class":188},[33,3552,3553,3556,3559,3561],{"class":35,"line":79},[33,3554,3555],{"class":180},"    const",[33,3557,3558],{"class":234}," keyMap",[33,3560,238],{"class":180},[33,3562,3563],{"class":188}," keyMapRef.current\n",[33,3565,3566,3568,3571,3573,3576,3578,3580,3582,3584],{"class":35,"line":85},[33,3567,3555],{"class":180},[33,3569,3570],{"class":234}," keys",[33,3572,238],{"class":180},[33,3574,3575],{"class":188}," items.",[33,3577,250],{"class":184},[33,3579,189],{"class":188},[33,3581,1889],{"class":192},[33,3583,258],{"class":180},[33,3585,392],{"class":188},[33,3587,3588],{"class":35,"line":91},[33,3589,3590],{"class":39},"      \u002F\u002F Reuse existing key if we've seen this item reference before\n",[33,3592,3593,3596,3599,3601,3604,3607,3610,3612],{"class":35,"line":97},[33,3594,3595],{"class":180},"      if",[33,3597,3598],{"class":188}," (keyMap.",[33,3600,336],{"class":184},[33,3602,3603],{"class":188},"(item)) ",[33,3605,3606],{"class":180},"return",[33,3608,3609],{"class":188}," keyMap.",[33,3611,365],{"class":184},[33,3613,3614],{"class":188},"(item)\n",[33,3616,3617],{"class":35,"line":103},[33,3618,3619],{"class":39},"      \u002F\u002F Generate a new stable key for new items\n",[33,3621,3622,3625,3627,3629,3632,3634,3637,3640,3643,3645,3648,3651,3653,3656],{"class":35,"line":109},[33,3623,3624],{"class":180},"      const",[33,3626,815],{"class":234},[33,3628,238],{"class":180},[33,3630,3631],{"class":188}," crypto.randomUUID ",[33,3633,2559],{"class":180},[33,3635,3636],{"class":188}," crypto.",[33,3638,3639],{"class":184},"randomUUID",[33,3641,3642],{"class":188},"() ",[33,3644,2571],{"class":180},[33,3646,3647],{"class":567}," `key-${",[33,3649,3650],{"class":188},"keyMap",[33,3652,1431],{"class":567},[33,3654,3655],{"class":188},"size",[33,3657,3658],{"class":567},"}`\n",[33,3660,3661,3664,3667],{"class":35,"line":114},[33,3662,3663],{"class":188},"      keyMap.",[33,3665,3666],{"class":184},"set",[33,3668,3669],{"class":188},"(item, key)\n",[33,3671,3672,3675],{"class":35,"line":120},[33,3673,3674],{"class":180},"      return",[33,3676,3677],{"class":188}," key\n",[33,3679,3680],{"class":35,"line":126},[33,3681,3682],{"class":188},"    })\n",[33,3684,3685],{"class":35,"line":132},[33,3686,3687],{"class":39},"    \u002F\u002F Clean up keys for items no longer present\n",[33,3689,3690,3692,3695,3697,3699,3702],{"class":35,"line":138},[33,3691,3555],{"class":180},[33,3693,3694],{"class":234}," currentSet",[33,3696,238],{"class":180},[33,3698,241],{"class":180},[33,3700,3701],{"class":184}," Set",[33,3703,3704],{"class":188},"(items)\n",[33,3706,3707,3710,3712,3714,3716,3718,3720,3722],{"class":35,"line":143},[33,3708,3709],{"class":180},"    for",[33,3711,300],{"class":188},[33,3713,303],{"class":180},[33,3715,1371],{"class":188},[33,3717,1889],{"class":234},[33,3719,450],{"class":188},[33,3721,453],{"class":180},[33,3723,3724],{"class":188}," keyMap) {\n",[33,3726,3727,3729,3731,3734,3737,3739,3742,3744],{"class":35,"line":149},[33,3728,3595],{"class":180},[33,3730,300],{"class":188},[33,3732,3733],{"class":180},"!",[33,3735,3736],{"class":188},"currentSet.",[33,3738,336],{"class":184},[33,3740,3741],{"class":188},"(item)) keyMap.",[33,3743,377],{"class":184},[33,3745,3614],{"class":188},[33,3747,3748],{"class":35,"line":155},[33,3749,415],{"class":188},[33,3751,3752,3755],{"class":35,"line":161},[33,3753,3754],{"class":180},"    return",[33,3756,3757],{"class":188}," keys\n",[33,3759,3760],{"class":35,"line":166},[33,3761,3762],{"class":188},"  }, [items])\n",[33,3764,3765],{"class":35,"line":172},[33,3766,541],{"class":188},[33,3768,3769],{"class":35,"line":177},[33,3770,3771],{"class":39},"\u002F\u002F Use case: items come from an external source without ids (e.g., parsed CSV rows).\n",[33,3773,3774],{"class":35,"line":205},[33,3775,3776],{"class":39},"\u002F\u002F The ref-based map ensures each item reference gets a stable key across renders.\n",[33,3778,3779],{"class":35,"line":211},[33,3780,59],{"emptyLinePlaceholder":58},[33,3782,3783],{"class":35,"line":217},[33,3784,3785],{"class":39},"\u002F\u002F ── 2. Virtualized list for large datasets (conceptual) ──\n",[33,3787,3788],{"class":35,"line":222},[33,3789,3790],{"class":39},"\u002F\u002F Rendering 10,000 \u003Cli> elements simultaneously will freeze the browser.\n",[33,3792,3793],{"class":35,"line":228},[33,3794,3795],{"class":39},"\u002F\u002F Virtualization renders only the visible window + a small overscan buffer.\n",[33,3797,3798],{"class":35,"line":264},[33,3799,3800],{"class":39},"\u002F\u002F Libraries: react-window, react-virtual, @tanstack\u002Freact-virtual.\n",[33,3802,3803],{"class":35,"line":277},[33,3804,59],{"emptyLinePlaceholder":58},[33,3806,3807,3809,3812,3814,3817,3819,3822,3824,3827,3829,3832],{"class":35,"line":289},[33,3808,181],{"class":180},[33,3810,3811],{"class":184}," useVirtualizedList",[33,3813,606],{"class":188},[33,3815,3816],{"class":192},"itemCount",[33,3818,196],{"class":188},[33,3820,3821],{"class":192},"itemHeight",[33,3823,196],{"class":188},[33,3825,3826],{"class":192},"viewportHeight",[33,3828,196],{"class":188},[33,3830,3831],{"class":192},"scrollTop",[33,3833,612],{"class":188},[33,3835,3836,3838,3841,3843,3846,3849,3851,3853,3856,3859,3862,3865,3868,3871,3874,3877],{"class":35,"line":294},[33,3837,231],{"class":180},[33,3839,3840],{"class":234}," startIndex",[33,3842,238],{"class":180},[33,3844,3845],{"class":188}," Math.",[33,3847,3848],{"class":184},"max",[33,3850,189],{"class":188},[33,3852,3292],{"class":234},[33,3854,3855],{"class":188},", Math.",[33,3857,3858],{"class":184},"floor",[33,3860,3861],{"class":188},"(scrollTop ",[33,3863,3864],{"class":180},"\u002F",[33,3866,3867],{"class":188}," itemHeight) ",[33,3869,3870],{"class":180},"-",[33,3872,3873],{"class":234}," 5",[33,3875,3876],{"class":188},")  ",[33,3878,3879],{"class":39},"\u002F\u002F 5 items overscan\n",[33,3881,3882,3884,3887,3889,3891,3894,3897,3899,3901,3903,3906],{"class":35,"line":315},[33,3883,231],{"class":180},[33,3885,3886],{"class":234}," visibleCount",[33,3888,238],{"class":180},[33,3890,3845],{"class":188},[33,3892,3893],{"class":184},"ceil",[33,3895,3896],{"class":188},"(viewportHeight ",[33,3898,3864],{"class":180},[33,3900,3867],{"class":188},[33,3902,3352],{"class":180},[33,3904,3905],{"class":234}," 10",[33,3907,3908],{"class":39},"         \u002F\u002F 10 items overscan\n",[33,3910,3911,3913,3916,3918,3920,3923,3926,3928],{"class":35,"line":342},[33,3912,231],{"class":180},[33,3914,3915],{"class":234}," endIndex",[33,3917,238],{"class":180},[33,3919,3845],{"class":188},[33,3921,3922],{"class":184},"min",[33,3924,3925],{"class":188},"(itemCount, startIndex ",[33,3927,3352],{"class":180},[33,3929,3930],{"class":188}," visibleCount)\n",[33,3932,3933],{"class":35,"line":348},[33,3934,59],{"emptyLinePlaceholder":58},[33,3936,3937,3939,3941,3943,3945],{"class":35,"line":371},[33,3938,617],{"class":180},[33,3940,3543],{"class":184},[33,3942,3546],{"class":188},[33,3944,951],{"class":180},[33,3946,3947],{"class":188}," ({\n",[33,3949,3950],{"class":35,"line":383},[33,3951,3952],{"class":188},"    startIndex,\n",[33,3954,3955],{"class":35,"line":395},[33,3956,3957],{"class":188},"    endIndex,\n",[33,3959,3960,3963,3965,3968,3970,3973,3976,3978,3981,3983,3985],{"class":35,"line":401},[33,3961,3962],{"class":188},"    visibleItems: Array.",[33,3964,564],{"class":184},[33,3966,3967],{"class":188},"({ length: endIndex ",[33,3969,3870],{"class":180},[33,3971,3972],{"class":188}," startIndex }, (",[33,3974,3975],{"class":192},"_",[33,3977,196],{"class":188},[33,3979,3980],{"class":192},"i",[33,3982,948],{"class":188},[33,3984,951],{"class":180},[33,3986,3947],{"class":188},[33,3988,3989,3992,3994],{"class":35,"line":412},[33,3990,3991],{"class":188},"      index: startIndex ",[33,3993,3352],{"class":180},[33,3995,3996],{"class":188}," i,\n",[33,3998,3999,4002,4004,4007,4010],{"class":35,"line":418},[33,4000,4001],{"class":188},"      offsetTop: (startIndex ",[33,4003,3352],{"class":180},[33,4005,4006],{"class":188}," i) ",[33,4008,4009],{"class":180},"*",[33,4011,4012],{"class":188}," itemHeight,\n",[33,4014,4015],{"class":35,"line":424},[33,4016,4017],{"class":188},"    })),\n",[33,4019,4020,4023,4025],{"class":35,"line":429},[33,4021,4022],{"class":188},"    totalHeight: itemCount ",[33,4024,4009],{"class":180},[33,4026,4012],{"class":188},[33,4028,4029],{"class":35,"line":435},[33,4030,4031],{"class":188},"  }), [startIndex, endIndex, itemCount, itemHeight])\n",[33,4033,4034],{"class":35,"line":459},[33,4035,541],{"class":188},[33,4037,4038],{"class":35,"line":471},[33,4039,4040],{"class":39},"\u002F\u002F Keys remain critical in virtualized lists — the visible window changes as the user\n",[33,4042,4043],{"class":35,"line":476},[33,4044,4045],{"class":39},"\u002F\u002F scrolls, and stable keys ensure React reuses (not remounts) items that scroll\n",[33,4047,4048],{"class":35,"line":481},[33,4049,4050],{"class":39},"\u002F\u002F back into view after scrolling out and back.\n",[33,4052,4053],{"class":35,"line":487},[33,4054,59],{"emptyLinePlaceholder":58},[33,4056,4057],{"class":35,"line":504},[33,4058,4059],{"class":39},"\u002F\u002F ── 3. Optimistic list updates (add\u002Fremove before server confirms) ──\n",[33,4061,4062,4064,4067,4069,4072,4074,4077,4079,4082],{"class":35,"line":513},[33,4063,181],{"class":180},[33,4065,4066],{"class":184}," useOptimisticList",[33,4068,189],{"class":188},[33,4070,4071],{"class":192},"fetchItems",[33,4073,196],{"class":188},[33,4075,4076],{"class":192},"createItem",[33,4078,196],{"class":188},[33,4080,4081],{"class":192},"deleteItem",[33,4083,202],{"class":188},[33,4085,4086,4088,4090,4092,4094,4097,4099,4101,4103],{"class":35,"line":518},[33,4087,231],{"class":180},[33,4089,1371],{"class":188},[33,4091,1861],{"class":234},[33,4093,196],{"class":188},[33,4095,4096],{"class":234},"setItems",[33,4098,450],{"class":188},[33,4100,818],{"class":180},[33,4102,2512],{"class":184},[33,4104,4105],{"class":188},"([])\n",[33,4107,4108,4110,4112,4115,4117,4120,4122,4124,4126,4128,4130,4132],{"class":35,"line":523},[33,4109,231],{"class":180},[33,4111,1371],{"class":188},[33,4113,4114],{"class":234},"pendingOps",[33,4116,196],{"class":188},[33,4118,4119],{"class":234},"setPendingOps",[33,4121,450],{"class":188},[33,4123,818],{"class":180},[33,4125,2512],{"class":184},[33,4127,189],{"class":188},[33,4129,3527],{"class":180},[33,4131,3701],{"class":184},[33,4133,3532],{"class":188},[33,4135,4136],{"class":35,"line":529},[33,4137,59],{"emptyLinePlaceholder":58},[33,4139,4140,4142,4145,4147,4150,4152,4155,4158,4160],{"class":35,"line":538},[33,4141,231],{"class":180},[33,4143,4144],{"class":234}," refresh",[33,4146,238],{"class":180},[33,4148,4149],{"class":184}," useCallback",[33,4151,189],{"class":188},[33,4153,4154],{"class":180},"async",[33,4156,4157],{"class":188}," () ",[33,4159,951],{"class":180},[33,4161,392],{"class":188},[33,4163,4164,4167,4169,4172,4175],{"class":35,"line":2146},[33,4165,4166],{"class":184},"    setItems",[33,4168,189],{"class":188},[33,4170,4171],{"class":180},"await",[33,4173,4174],{"class":184}," fetchItems",[33,4176,3532],{"class":188},[33,4178,4179],{"class":35,"line":2168},[33,4180,4181],{"class":188},"  }, [fetchItems])\n",[33,4183,4184],{"class":35,"line":2174},[33,4185,59],{"emptyLinePlaceholder":58},[33,4187,4188,4190,4193,4195,4197,4199,4201,4203,4206,4208,4210],{"class":35,"line":2179},[33,4189,231],{"class":180},[33,4191,4192],{"class":234}," optimisticAdd",[33,4194,238],{"class":180},[33,4196,4149],{"class":184},[33,4198,189],{"class":188},[33,4200,4154],{"class":180},[33,4202,300],{"class":188},[33,4204,4205],{"class":192},"newItemData",[33,4207,948],{"class":188},[33,4209,951],{"class":180},[33,4211,392],{"class":188},[33,4213,4214,4216,4219,4221,4224,4227,4229,4232,4234],{"class":35,"line":2193},[33,4215,3555],{"class":180},[33,4217,4218],{"class":234}," tempId",[33,4220,238],{"class":180},[33,4222,4223],{"class":567}," `temp-${",[33,4225,4226],{"class":188},"Date",[33,4228,1431],{"class":567},[33,4230,4231],{"class":184},"now",[33,4233,1483],{"class":567},[33,4235,3658],{"class":567},[33,4237,4238,4240,4243,4245,4248,4250,4253,4255],{"class":35,"line":2198},[33,4239,3555],{"class":180},[33,4241,4242],{"class":234}," optimisticItem",[33,4244,238],{"class":180},[33,4246,4247],{"class":188}," { ",[33,4249,2534],{"class":180},[33,4251,4252],{"class":188},"newItemData, id: tempId, _pending: ",[33,4254,2517],{"class":234},[33,4256,4257],{"class":188}," }\n",[33,4259,4260,4262,4264,4267,4269,4271,4273],{"class":35,"line":2203},[33,4261,4166],{"class":184},[33,4263,189],{"class":188},[33,4265,4266],{"class":192},"prev",[33,4268,258],{"class":180},[33,4270,1371],{"class":188},[33,4272,2534],{"class":180},[33,4274,4275],{"class":188},"prev, optimisticItem])\n",[33,4277,4278,4281,4283,4285,4287,4289,4291,4294,4296],{"class":35,"line":2208},[33,4279,4280],{"class":184},"    setPendingOps",[33,4282,189],{"class":188},[33,4284,4266],{"class":192},[33,4286,258],{"class":180},[33,4288,241],{"class":180},[33,4290,3701],{"class":184},[33,4292,4293],{"class":188},"([",[33,4295,2534],{"class":180},[33,4297,4298],{"class":188},"prev, tempId]))\n",[33,4300,4301,4304],{"class":35,"line":2214},[33,4302,4303],{"class":180},"    try",[33,4305,392],{"class":188},[33,4307,4308,4310,4313,4315,4318,4321],{"class":35,"line":2228},[33,4309,3624],{"class":180},[33,4311,4312],{"class":234}," realItem",[33,4314,238],{"class":180},[33,4316,4317],{"class":180}," await",[33,4319,4320],{"class":184}," createItem",[33,4322,4323],{"class":188},"(newItemData)\n",[33,4325,4326,4329,4331,4333,4335,4338,4340,4342,4344,4346,4349,4352,4355,4357,4360,4362],{"class":35,"line":2235},[33,4327,4328],{"class":184},"      setItems",[33,4330,189],{"class":188},[33,4332,4266],{"class":192},[33,4334,258],{"class":180},[33,4336,4337],{"class":188}," prev.",[33,4339,250],{"class":184},[33,4341,189],{"class":188},[33,4343,1889],{"class":192},[33,4345,258],{"class":180},[33,4347,4348],{"class":188}," item.id ",[33,4350,4351],{"class":180},"===",[33,4353,4354],{"class":188}," tempId ",[33,4356,2559],{"class":180},[33,4358,4359],{"class":188}," realItem ",[33,4361,2571],{"class":180},[33,4363,4364],{"class":188}," item))\n",[33,4366,4367,4369,4372],{"class":35,"line":2244},[33,4368,386],{"class":188},[33,4370,4371],{"class":180},"catch",[33,4373,4374],{"class":188}," (err) {\n",[33,4376,4377,4379,4381,4383,4385,4387,4390,4392,4394,4396,4398,4400,4403],{"class":35,"line":2262},[33,4378,4328],{"class":184},[33,4380,189],{"class":188},[33,4382,4266],{"class":192},[33,4384,258],{"class":180},[33,4386,4337],{"class":188},[33,4388,4389],{"class":184},"filter",[33,4391,189],{"class":188},[33,4393,1889],{"class":192},[33,4395,258],{"class":180},[33,4397,4348],{"class":188},[33,4399,324],{"class":180},[33,4401,4402],{"class":188}," tempId))  ",[33,4404,4405],{"class":39},"\u002F\u002F rollback\n",[33,4407,4408,4411],{"class":35,"line":2272},[33,4409,4410],{"class":180},"      throw",[33,4412,4413],{"class":188}," err\n",[33,4415,4416,4418,4421],{"class":35,"line":2281},[33,4417,386],{"class":188},[33,4419,4420],{"class":180},"finally",[33,4422,392],{"class":188},[33,4424,4425,4428,4430,4432,4434,4436,4438,4441,4443,4445,4447,4450,4452,4455,4457],{"class":35,"line":2286},[33,4426,4427],{"class":184},"      setPendingOps",[33,4429,189],{"class":188},[33,4431,4266],{"class":192},[33,4433,258],{"class":180},[33,4435,4247],{"class":188},[33,4437,303],{"class":180},[33,4439,4440],{"class":234}," next",[33,4442,238],{"class":180},[33,4444,241],{"class":180},[33,4446,3701],{"class":184},[33,4448,4449],{"class":188},"(prev); next.",[33,4451,377],{"class":184},[33,4453,4454],{"class":188},"(tempId); ",[33,4456,3606],{"class":180},[33,4458,4459],{"class":188}," next })\n",[33,4461,4462],{"class":35,"line":2291},[33,4463,415],{"class":188},[33,4465,4466],{"class":35,"line":2296},[33,4467,4468],{"class":188},"  }, [createItem])\n",[33,4470,4471],{"class":35,"line":2302},[33,4472,59],{"emptyLinePlaceholder":58},[33,4474,4475,4477,4480,4482,4484,4486,4488,4490,4493,4495,4497],{"class":35,"line":2317},[33,4476,231],{"class":180},[33,4478,4479],{"class":234}," optimisticRemove",[33,4481,238],{"class":180},[33,4483,4149],{"class":184},[33,4485,189],{"class":188},[33,4487,4154],{"class":180},[33,4489,300],{"class":188},[33,4491,4492],{"class":192},"id",[33,4494,948],{"class":188},[33,4496,951],{"class":180},[33,4498,392],{"class":188},[33,4500,4501,4503,4506,4508,4510,4513,4515,4517,4519,4521,4523],{"class":35,"line":2324},[33,4502,3555],{"class":180},[33,4504,4505],{"class":234}," snapshot",[33,4507,238],{"class":180},[33,4509,3575],{"class":188},[33,4511,4512],{"class":184},"find",[33,4514,189],{"class":188},[33,4516,1889],{"class":192},[33,4518,258],{"class":180},[33,4520,4348],{"class":188},[33,4522,4351],{"class":180},[33,4524,4525],{"class":188}," id)\n",[33,4527,4528,4530,4532,4534,4536,4538,4540,4542,4544,4546,4548,4550],{"class":35,"line":2334},[33,4529,4166],{"class":184},[33,4531,189],{"class":188},[33,4533,4266],{"class":192},[33,4535,258],{"class":180},[33,4537,4337],{"class":188},[33,4539,4389],{"class":184},[33,4541,189],{"class":188},[33,4543,1889],{"class":192},[33,4545,258],{"class":180},[33,4547,4348],{"class":188},[33,4549,324],{"class":180},[33,4551,4552],{"class":188}," id))\n",[33,4554,4555,4557],{"class":35,"line":2351},[33,4556,4303],{"class":180},[33,4558,392],{"class":188},[33,4560,4561,4564,4567],{"class":35,"line":2357},[33,4562,4563],{"class":180},"      await",[33,4565,4566],{"class":184}," deleteItem",[33,4568,4569],{"class":188},"(id)\n",[33,4571,4572,4574,4576],{"class":35,"line":2363},[33,4573,386],{"class":188},[33,4575,4371],{"class":180},[33,4577,4374],{"class":188},[33,4579,4580,4582,4585,4587,4589,4591,4593,4595,4597,4599,4601,4603,4605,4608,4610,4612,4614,4617,4619,4621,4623,4626,4628,4631,4633],{"class":35,"line":2378},[33,4581,3595],{"class":180},[33,4583,4584],{"class":188}," (snapshot) ",[33,4586,4096],{"class":184},[33,4588,189],{"class":188},[33,4590,4266],{"class":192},[33,4592,258],{"class":180},[33,4594,4247],{"class":188},[33,4596,303],{"class":180},[33,4598,4440],{"class":234},[33,4600,238],{"class":180},[33,4602,1371],{"class":188},[33,4604,2534],{"class":180},[33,4606,4607],{"class":188},"prev, snapshot]; next.",[33,4609,2540],{"class":184},[33,4611,938],{"class":188},[33,4613,1455],{"class":192},[33,4615,4616],{"class":188},",",[33,4618,2549],{"class":192},[33,4620,948],{"class":188},[33,4622,951],{"class":180},[33,4624,4625],{"class":188}," a.id ",[33,4627,3870],{"class":180},[33,4629,4630],{"class":188}," b.id); ",[33,4632,3606],{"class":180},[33,4634,4459],{"class":188},[33,4636,4637,4639],{"class":35,"line":2393},[33,4638,4410],{"class":180},[33,4640,4413],{"class":188},[33,4642,4643],{"class":35,"line":2408},[33,4644,415],{"class":188},[33,4646,4647],{"class":35,"line":2417},[33,4648,4649],{"class":188},"  }, [deleteItem, items])\n",[33,4651,4652],{"class":35,"line":2422},[33,4653,59],{"emptyLinePlaceholder":58},[33,4655,4656,4658],{"class":35,"line":2431},[33,4657,617],{"class":180},[33,4659,4660],{"class":188}," { items, pendingOps, refresh, optimisticAdd, optimisticRemove }\n",[33,4662,4663],{"class":35,"line":2436},[33,4664,541],{"class":188},[33,4666,4668],{"class":35,"line":4667},90,[33,4669,4670],{"class":39},"\u002F\u002F The tempId key ensures the optimistic item is uniquely keyed. When the server\n",[33,4672,4674],{"class":35,"line":4673},91,[33,4675,4676],{"class":39},"\u002F\u002F responds with a real id, the key changes → React unmounts the temp item and\n",[33,4678,4680],{"class":35,"line":4679},92,[33,4681,4682],{"class":39},"\u002F\u002F mounts the real one. This is the one case where a key change on the same logical\n",[33,4684,4686],{"class":35,"line":4685},93,[33,4687,4688],{"class":39},"\u002F\u002F item is intentional and correct.\n",[14,4690,4692],{"id":4691},"tips-tricks","💡 Tips & Tricks",[19,4694,4696],{"filename":4695,"language":22},"tips.js",[24,4697,4699],{"className":26,"code":4698,"language":22,"meta":28,"style":28},"\u002F\u002F [Debug] \"Each child in a list should have a unique key prop\" warning = fix it NOW.\n\u002F\u002F It fires precisely in the scenario most likely to cause positional-identity bugs.\n\n\u002F\u002F [Idiom] Prefer a stable field from the data (database id, UUID) over anything\n\u002F\u002F derived at render time. `${item.name}-${index}` reintroduces instability the\n\u002F\u002F moment two items share a name or the list reorders.\n\n\u002F\u002F [Performance] Changing a key to force remount is cheap for small components but\n\u002F\u002F discards the ENTIRE subtree (DOM, state, effects). Use it for genuine resets,\n\u002F\u002F not as a \"force update\" hammer.\n\n\u002F\u002F [Debug] If a list item's local state \"jumps\" to the wrong row after delete\u002Fsort,\n\u002F\u002F suspect index-as-key before anything else. This symptom is the signature of\n\u002F\u002F positional identity mismatched against reordered data.\n\n\u002F\u002F [Idiom] Use \u003CFragment key={id}> for multiple sibling elements per list item —\n\u002F\u002F keeps DOM flat while giving reconciliation the identity it needs. The shorthand\n\u002F\u002F \u003C>...\u003C\u002F> cannot take a key prop.\n\n\u002F\u002F [Performance] For lists > 100 items, use virtualization (react-window, @tanstack\u002Freact-virtual).\n\u002F\u002F Keys remain critical — stable keys ensure scrolled-out items reuse, not remount,\n\u002F\u002F when they scroll back into view.\n\n\u002F\u002F [Idiom] When data lacks natural ids, use a ref-based Map to assign stable keys\n\u002F\u002F per item reference — never Math.random() or Date.now() per render.\n",[30,4700,4701,4706,4711,4715,4720,4725,4730,4734,4739,4744,4749,4753,4758,4763,4768,4772,4777,4782,4787,4791,4796,4801,4806,4810,4815],{"__ignoreMap":28},[33,4702,4703],{"class":35,"line":36},[33,4704,4705],{"class":39},"\u002F\u002F [Debug] \"Each child in a list should have a unique key prop\" warning = fix it NOW.\n",[33,4707,4708],{"class":35,"line":43},[33,4709,4710],{"class":39},"\u002F\u002F It fires precisely in the scenario most likely to cause positional-identity bugs.\n",[33,4712,4713],{"class":35,"line":49},[33,4714,59],{"emptyLinePlaceholder":58},[33,4716,4717],{"class":35,"line":55},[33,4718,4719],{"class":39},"\u002F\u002F [Idiom] Prefer a stable field from the data (database id, UUID) over anything\n",[33,4721,4722],{"class":35,"line":62},[33,4723,4724],{"class":39},"\u002F\u002F derived at render time. `${item.name}-${index}` reintroduces instability the\n",[33,4726,4727],{"class":35,"line":68},[33,4728,4729],{"class":39},"\u002F\u002F moment two items share a name or the list reorders.\n",[33,4731,4732],{"class":35,"line":74},[33,4733,59],{"emptyLinePlaceholder":58},[33,4735,4736],{"class":35,"line":79},[33,4737,4738],{"class":39},"\u002F\u002F [Performance] Changing a key to force remount is cheap for small components but\n",[33,4740,4741],{"class":35,"line":85},[33,4742,4743],{"class":39},"\u002F\u002F discards the ENTIRE subtree (DOM, state, effects). Use it for genuine resets,\n",[33,4745,4746],{"class":35,"line":91},[33,4747,4748],{"class":39},"\u002F\u002F not as a \"force update\" hammer.\n",[33,4750,4751],{"class":35,"line":97},[33,4752,59],{"emptyLinePlaceholder":58},[33,4754,4755],{"class":35,"line":103},[33,4756,4757],{"class":39},"\u002F\u002F [Debug] If a list item's local state \"jumps\" to the wrong row after delete\u002Fsort,\n",[33,4759,4760],{"class":35,"line":109},[33,4761,4762],{"class":39},"\u002F\u002F suspect index-as-key before anything else. This symptom is the signature of\n",[33,4764,4765],{"class":35,"line":114},[33,4766,4767],{"class":39},"\u002F\u002F positional identity mismatched against reordered data.\n",[33,4769,4770],{"class":35,"line":120},[33,4771,59],{"emptyLinePlaceholder":58},[33,4773,4774],{"class":35,"line":126},[33,4775,4776],{"class":39},"\u002F\u002F [Idiom] Use \u003CFragment key={id}> for multiple sibling elements per list item —\n",[33,4778,4779],{"class":35,"line":132},[33,4780,4781],{"class":39},"\u002F\u002F keeps DOM flat while giving reconciliation the identity it needs. The shorthand\n",[33,4783,4784],{"class":35,"line":138},[33,4785,4786],{"class":39},"\u002F\u002F \u003C>...\u003C\u002F> cannot take a key prop.\n",[33,4788,4789],{"class":35,"line":143},[33,4790,59],{"emptyLinePlaceholder":58},[33,4792,4793],{"class":35,"line":149},[33,4794,4795],{"class":39},"\u002F\u002F [Performance] For lists > 100 items, use virtualization (react-window, @tanstack\u002Freact-virtual).\n",[33,4797,4798],{"class":35,"line":155},[33,4799,4800],{"class":39},"\u002F\u002F Keys remain critical — stable keys ensure scrolled-out items reuse, not remount,\n",[33,4802,4803],{"class":35,"line":161},[33,4804,4805],{"class":39},"\u002F\u002F when they scroll back into view.\n",[33,4807,4808],{"class":35,"line":166},[33,4809,59],{"emptyLinePlaceholder":58},[33,4811,4812],{"class":35,"line":172},[33,4813,4814],{"class":39},"\u002F\u002F [Idiom] When data lacks natural ids, use a ref-based Map to assign stable keys\n",[33,4816,4817],{"class":35,"line":177},[33,4818,4819],{"class":39},"\u002F\u002F per item reference — never Math.random() or Date.now() per render.\n",[14,4821,4823],{"id":4822},"️-edge-cases-gotchas","⚠️ Edge Cases & Gotchas",[19,4825,4827],{"filename":4826,"language":22},"edge_cases.js",[24,4828,4830],{"className":26,"code":4829,"language":22,"meta":28,"style":28},"\u002F\u002F [Gotcha] Index-as-key silently corrupts per-item state on reorder, insert-at-start,\n\u002F\u002F or delete-from-middle. No error, no warning — uncontrolled inputs, local useState,\n\u002F\u002F and CSS transitions end up attached to the wrong data row. The hardest bug to trace\n\u002F\u002F because nothing throws — the UI just looks wrong.\n\n\u002F\u002F [Gotcha] key={Math.random()} is worse than no key at all. It guarantees every item\n\u002F\u002F looks brand-new on every render → full unmount\u002Fremount → loses focus, scroll position,\n\u002F\u002F all per-item state, every CSS transition restarts. A performance and UX disaster.\n\n\u002F\u002F [Gotcha] The shorthand \u003C>...\u003C\u002F> Fragment syntax CANNOT take a key prop.\n\u002F\u002F \u003Ckey={id}>...\u003C\u002F> is a syntax error. Must use \u003CFragment key={id}> (imported from 'react').\n\n\u002F\u002F [Gotcha] A key placed on an element NESTED inside a list item's returned JSX has\n\u002F\u002F NO effect on reconciliation. React only reads key off elements that are direct\n\u002F\u002F children of the array\u002Fiterable being rendered.\n\u002F\u002F   \u003Cli>\u003Cspan key={id}>...\u003C\u002Fspan>\u003C\u002Fli>  ← key on \u003Cspan> is useless if \u003Cli> is the mapped element.\n\n\u002F\u002F [Gotcha] Keys are NOT required to be globally unique across the app — only among\n\u002F\u002F siblings in the same array. Two independent lists reusing the same key values is safe.\n\u002F\u002F Reusing the same key WITHIN one list is a bug (duplicate keys → React warns + keeps only one).\n\n\u002F\u002F [Gotcha] Changing a key on an item that should be preserved (e.g., keying by a value\n\u002F\u002F that changes on every update) causes unnecessary remounts. Keys should be stable for\n\u002F\u002F the lifetime of the item in the list — only change when the item's identity truly changes.\n\n\u002F\u002F [Gotcha] defaultChecked\u002FdefaultValue on uncontrolled inputs inside list items with\n\u002F\u002F index keys: these props only apply on MOUNT, not on reuse. After a reorder, React\n\u002F\u002F reuses the DOM node but does NOT re-apply defaultChecked → stale checkbox state.\n",[30,4831,4832,4837,4842,4847,4852,4856,4861,4866,4871,4875,4880,4885,4889,4894,4899,4904,4909,4913,4918,4923,4928,4932,4937,4942,4947,4951,4956,4961],{"__ignoreMap":28},[33,4833,4834],{"class":35,"line":36},[33,4835,4836],{"class":39},"\u002F\u002F [Gotcha] Index-as-key silently corrupts per-item state on reorder, insert-at-start,\n",[33,4838,4839],{"class":35,"line":43},[33,4840,4841],{"class":39},"\u002F\u002F or delete-from-middle. No error, no warning — uncontrolled inputs, local useState,\n",[33,4843,4844],{"class":35,"line":49},[33,4845,4846],{"class":39},"\u002F\u002F and CSS transitions end up attached to the wrong data row. The hardest bug to trace\n",[33,4848,4849],{"class":35,"line":55},[33,4850,4851],{"class":39},"\u002F\u002F because nothing throws — the UI just looks wrong.\n",[33,4853,4854],{"class":35,"line":62},[33,4855,59],{"emptyLinePlaceholder":58},[33,4857,4858],{"class":35,"line":68},[33,4859,4860],{"class":39},"\u002F\u002F [Gotcha] key={Math.random()} is worse than no key at all. It guarantees every item\n",[33,4862,4863],{"class":35,"line":74},[33,4864,4865],{"class":39},"\u002F\u002F looks brand-new on every render → full unmount\u002Fremount → loses focus, scroll position,\n",[33,4867,4868],{"class":35,"line":79},[33,4869,4870],{"class":39},"\u002F\u002F all per-item state, every CSS transition restarts. A performance and UX disaster.\n",[33,4872,4873],{"class":35,"line":85},[33,4874,59],{"emptyLinePlaceholder":58},[33,4876,4877],{"class":35,"line":91},[33,4878,4879],{"class":39},"\u002F\u002F [Gotcha] The shorthand \u003C>...\u003C\u002F> Fragment syntax CANNOT take a key prop.\n",[33,4881,4882],{"class":35,"line":97},[33,4883,4884],{"class":39},"\u002F\u002F \u003Ckey={id}>...\u003C\u002F> is a syntax error. Must use \u003CFragment key={id}> (imported from 'react').\n",[33,4886,4887],{"class":35,"line":103},[33,4888,59],{"emptyLinePlaceholder":58},[33,4890,4891],{"class":35,"line":109},[33,4892,4893],{"class":39},"\u002F\u002F [Gotcha] A key placed on an element NESTED inside a list item's returned JSX has\n",[33,4895,4896],{"class":35,"line":114},[33,4897,4898],{"class":39},"\u002F\u002F NO effect on reconciliation. React only reads key off elements that are direct\n",[33,4900,4901],{"class":35,"line":120},[33,4902,4903],{"class":39},"\u002F\u002F children of the array\u002Fiterable being rendered.\n",[33,4905,4906],{"class":35,"line":126},[33,4907,4908],{"class":39},"\u002F\u002F   \u003Cli>\u003Cspan key={id}>...\u003C\u002Fspan>\u003C\u002Fli>  ← key on \u003Cspan> is useless if \u003Cli> is the mapped element.\n",[33,4910,4911],{"class":35,"line":132},[33,4912,59],{"emptyLinePlaceholder":58},[33,4914,4915],{"class":35,"line":138},[33,4916,4917],{"class":39},"\u002F\u002F [Gotcha] Keys are NOT required to be globally unique across the app — only among\n",[33,4919,4920],{"class":35,"line":143},[33,4921,4922],{"class":39},"\u002F\u002F siblings in the same array. Two independent lists reusing the same key values is safe.\n",[33,4924,4925],{"class":35,"line":149},[33,4926,4927],{"class":39},"\u002F\u002F Reusing the same key WITHIN one list is a bug (duplicate keys → React warns + keeps only one).\n",[33,4929,4930],{"class":35,"line":155},[33,4931,59],{"emptyLinePlaceholder":58},[33,4933,4934],{"class":35,"line":161},[33,4935,4936],{"class":39},"\u002F\u002F [Gotcha] Changing a key on an item that should be preserved (e.g., keying by a value\n",[33,4938,4939],{"class":35,"line":166},[33,4940,4941],{"class":39},"\u002F\u002F that changes on every update) causes unnecessary remounts. Keys should be stable for\n",[33,4943,4944],{"class":35,"line":172},[33,4945,4946],{"class":39},"\u002F\u002F the lifetime of the item in the list — only change when the item's identity truly changes.\n",[33,4948,4949],{"class":35,"line":177},[33,4950,59],{"emptyLinePlaceholder":58},[33,4952,4953],{"class":35,"line":205},[33,4954,4955],{"class":39},"\u002F\u002F [Gotcha] defaultChecked\u002FdefaultValue on uncontrolled inputs inside list items with\n",[33,4957,4958],{"class":35,"line":211},[33,4959,4960],{"class":39},"\u002F\u002F index keys: these props only apply on MOUNT, not on reuse. After a reorder, React\n",[33,4962,4963],{"class":35,"line":217},[33,4964,4965],{"class":39},"\u002F\u002F reuses the DOM node but does NOT re-apply defaultChecked → stale checkbox state.\n",[14,4967,4969],{"id":4968},"spot-the-bug","🧠 Spot the Bug",[4971,4972,4973],"p",{},"A \"recently viewed products\" carousel lets users remove an item. After removing the first product, the wrong product's \"Remove\" button ends up disabled mid-animation:",[19,4975,4977],{"filename":4976,"language":22},"spot_the_bug.js",[24,4978,4980],{"className":26,"code":4979,"language":22,"meta":28,"style":28},"function RecentlyViewed({ products, onRemove }) {\n  return (\n    \u003Cdiv className=\"carousel\">\n      {products.map((product, index) => (\n        \u003CProductCard\n          key={index}\n          product={product}\n          onRemove={() => onRemove(product.id)}\n        \u002F>\n      ))}\n    \u003C\u002Fdiv>\n  )\n}\n\nfunction ProductCard({ product, onRemove }) {\n  const [removing, setRemoving] = useState(false)\n\n  function handleClick() {\n    setRemoving(true)\n    setTimeout(() => onRemove(), 300)  \u002F\u002F 300ms fade-out animation before removal\n  }\n\n  return (\n    \u003Cdiv className={removing ? 'fading-out' : ''}>\n      \u003Cspan>{product.name}\u003C\u002Fspan>\n      \u003Cbutton disabled={removing} onClick={handleClick}>Remove\u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n  )\n}\n",[30,4981,4982,5000,5006,5021,5043,5050,5060,5070,5086,5091,5095,5103,5107,5111,5115,5132,5158,5162,5172,5183,5205,5209,5213,5219,5245,5258,5284,5292,5296],{"__ignoreMap":28},[33,4983,4984,4986,4989,4991,4994,4996,4998],{"class":35,"line":36},[33,4985,181],{"class":180},[33,4987,4988],{"class":184}," RecentlyViewed",[33,4990,606],{"class":188},[33,4992,4993],{"class":192},"products",[33,4995,196],{"class":188},[33,4997,913],{"class":192},[33,4999,612],{"class":188},[33,5001,5002,5004],{"class":35,"line":43},[33,5003,617],{"class":180},[33,5005,620],{"class":188},[33,5007,5008,5010,5012,5014,5016,5019],{"class":35,"line":49},[33,5009,625],{"class":188},[33,5011,2596],{"class":628},[33,5013,3092],{"class":184},[33,5015,818],{"class":180},[33,5017,5018],{"class":567},"\"carousel\"",[33,5020,632],{"class":188},[33,5022,5023,5026,5028,5030,5033,5035,5037,5039,5041],{"class":35,"line":55},[33,5024,5025],{"class":188},"      {products.",[33,5027,250],{"class":184},[33,5029,938],{"class":188},[33,5031,5032],{"class":192},"product",[33,5034,196],{"class":188},[33,5036,945],{"class":192},[33,5038,948],{"class":188},[33,5040,951],{"class":180},[33,5042,620],{"class":188},[33,5044,5045,5047],{"class":35,"line":62},[33,5046,810],{"class":188},[33,5048,5049],{"class":234},"ProductCard\n",[33,5051,5052,5055,5057],{"class":35,"line":68},[33,5053,5054],{"class":184},"          key",[33,5056,818],{"class":180},[33,5058,5059],{"class":188},"{index}\n",[33,5061,5062,5065,5067],{"class":35,"line":74},[33,5063,5064],{"class":184},"          product",[33,5066,818],{"class":180},[33,5068,5069],{"class":188},"{product}\n",[33,5071,5072,5075,5077,5079,5081,5083],{"class":35,"line":79},[33,5073,5074],{"class":184},"          onRemove",[33,5076,818],{"class":180},[33,5078,1027],{"class":188},[33,5080,951],{"class":180},[33,5082,1032],{"class":184},[33,5084,5085],{"class":188},"(product.id)}\n",[33,5087,5088],{"class":35,"line":85},[33,5089,5090],{"class":188},"        \u002F>\n",[33,5092,5093],{"class":35,"line":91},[33,5094,830],{"class":188},[33,5096,5097,5099,5101],{"class":35,"line":97},[33,5098,675],{"class":188},[33,5100,2596],{"class":628},[33,5102,632],{"class":188},[33,5104,5105],{"class":35,"line":103},[33,5106,684],{"class":188},[33,5108,5109],{"class":35,"line":109},[33,5110,541],{"class":188},[33,5112,5113],{"class":35,"line":114},[33,5114,59],{"emptyLinePlaceholder":58},[33,5116,5117,5119,5122,5124,5126,5128,5130],{"class":35,"line":120},[33,5118,181],{"class":180},[33,5120,5121],{"class":184}," ProductCard",[33,5123,606],{"class":188},[33,5125,5032],{"class":192},[33,5127,196],{"class":188},[33,5129,913],{"class":192},[33,5131,612],{"class":188},[33,5133,5134,5136,5138,5141,5143,5146,5148,5150,5152,5154,5156],{"class":35,"line":126},[33,5135,231],{"class":180},[33,5137,1371],{"class":188},[33,5139,5140],{"class":234},"removing",[33,5142,196],{"class":188},[33,5144,5145],{"class":234},"setRemoving",[33,5147,450],{"class":188},[33,5149,818],{"class":180},[33,5151,2512],{"class":184},[33,5153,189],{"class":188},[33,5155,3006],{"class":234},[33,5157,2520],{"class":188},[33,5159,5160],{"class":35,"line":132},[33,5161,59],{"emptyLinePlaceholder":58},[33,5163,5164,5167,5170],{"class":35,"line":138},[33,5165,5166],{"class":180},"  function",[33,5168,5169],{"class":184}," handleClick",[33,5171,1406],{"class":188},[33,5173,5174,5177,5179,5181],{"class":35,"line":143},[33,5175,5176],{"class":184},"    setRemoving",[33,5178,189],{"class":188},[33,5180,2517],{"class":234},[33,5182,2520],{"class":188},[33,5184,5185,5188,5190,5192,5194,5197,5200,5202],{"class":35,"line":149},[33,5186,5187],{"class":184},"    setTimeout",[33,5189,3546],{"class":188},[33,5191,951],{"class":180},[33,5193,1032],{"class":184},[33,5195,5196],{"class":188},"(), ",[33,5198,5199],{"class":234},"300",[33,5201,3876],{"class":188},[33,5203,5204],{"class":39},"\u002F\u002F 300ms fade-out animation before removal\n",[33,5206,5207],{"class":35,"line":155},[33,5208,421],{"class":188},[33,5210,5211],{"class":35,"line":161},[33,5212,59],{"emptyLinePlaceholder":58},[33,5214,5215,5217],{"class":35,"line":166},[33,5216,617],{"class":180},[33,5218,620],{"class":188},[33,5220,5221,5223,5225,5227,5229,5232,5234,5237,5239,5242],{"class":35,"line":172},[33,5222,625],{"class":188},[33,5224,2596],{"class":628},[33,5226,3092],{"class":184},[33,5228,818],{"class":180},[33,5230,5231],{"class":188},"{removing ",[33,5233,2559],{"class":180},[33,5235,5236],{"class":567}," 'fading-out'",[33,5238,3064],{"class":180},[33,5240,5241],{"class":567}," ''",[33,5243,5244],{"class":188},"}>\n",[33,5246,5247,5249,5251,5254,5256],{"class":35,"line":177},[33,5248,1726],{"class":188},[33,5250,33],{"class":628},[33,5252,5253],{"class":188},">{product.name}\u003C\u002F",[33,5255,33],{"class":628},[33,5257,632],{"class":188},[33,5259,5260,5262,5264,5267,5269,5272,5275,5277,5280,5282],{"class":35,"line":205},[33,5261,1726],{"class":188},[33,5263,1019],{"class":628},[33,5265,5266],{"class":184}," disabled",[33,5268,818],{"class":180},[33,5270,5271],{"class":188},"{removing} ",[33,5273,5274],{"class":184},"onClick",[33,5276,818],{"class":180},[33,5278,5279],{"class":188},"{handleClick}>Remove\u003C\u002F",[33,5281,1019],{"class":628},[33,5283,632],{"class":188},[33,5285,5286,5288,5290],{"class":35,"line":211},[33,5287,675],{"class":188},[33,5289,2596],{"class":628},[33,5291,632],{"class":188},[33,5293,5294],{"class":35,"line":217},[33,5295,684],{"class":188},[33,5297,5298],{"class":35,"line":222},[33,5299,541],{"class":188},[5301,5302,5303,5307,5342,5355,5394],"details",{},[5304,5305,5306],"summary",{},"Answer",[4971,5308,5309,5312,5313,5315,5316,5318,5319,5322,5323,5325,5326,5328,5329,5331,5332,5334,5335,5338,5339,5341],{},[30,5310,5311],{},"ProductCard"," holds local ",[30,5314,5140],{}," state, and the list is keyed by array ",[30,5317,945],{}," instead of ",[30,5320,5321],{},"product.id",". Clicking \"Remove\" on the first product sets that instance's ",[30,5324,5140],{}," to ",[30,5327,2517],{}," and starts the 300ms fade. The parent's ",[30,5330,4993],{}," array only updates after the timeout fires. On a second quick removal before the first timeout completes: removing an earlier item shifts every subsequent item's index down by one. React matches the new index-0 element against the old index-0 ",[30,5333,5311],{}," instance — which has ",[30,5336,5337],{},"removing: true"," mid-animation — and hands it the next product's data while keeping its old ",[30,5340,5140],{}," state. The fade-out and disabled button end up on the wrong product.",[4971,5343,5344,5348,5349,5351,5352,5354],{},[5345,5346,5347],"strong",{},"Fix",": key by ",[30,5350,5321],{}," so each ",[30,5353,5311],{}," instance's state follows the product, not the position:",[24,5356,5358],{"className":26,"code":5357,"language":22,"meta":28,"style":28},"\u003CProductCard key={product.id} product={product} onRemove={() => onRemove(product.id)} \u002F>\n",[30,5359,5360],{"__ignoreMap":28},[33,5361,5362,5365,5367,5369,5371,5374,5376,5378,5381,5383,5385,5387,5389,5391],{"class":35,"line":36},[33,5363,5364],{"class":188},"\u003C",[33,5366,5311],{"class":234},[33,5368,815],{"class":184},[33,5370,818],{"class":180},[33,5372,5373],{"class":188},"{product.id} ",[33,5375,5032],{"class":184},[33,5377,818],{"class":180},[33,5379,5380],{"class":188},"{product} ",[33,5382,913],{"class":184},[33,5384,818],{"class":180},[33,5386,1027],{"class":188},[33,5388,951],{"class":180},[33,5390,1032],{"class":184},[33,5392,5393],{"class":188},"(product.id)} \u002F>\n",[4971,5395,5396,5397,5399],{},"With stable keys, React matches by product identity — the instance with ",[30,5398,5337],{}," is correctly removed when its timeout fires, and no other product's state is affected.",[14,5401,5403],{"id":5402},"key-takeaways","Key Takeaways",[19,5405,5407],{"filename":5406,"language":22},"key_takeaways.js",[24,5408,5410],{"className":26,"code":5409,"language":22,"meta":28,"style":28},"\u002F\u002F 1. Reconciliation diffs React element trees (not real DOM) using an O(n) heuristic:\n\u002F\u002F    different types → full teardown; same type + same position → update in place.\n\u002F\u002F    Keys correct the \"same position\" assumption for lists.\n\n\u002F\u002F 2. Without keys, React matches array children POSITIONALLY — index-0 to index-0.\n\u002F\u002F    This silently misattributes per-item DOM\u002Fstate on reorder, filter, or delete-from-middle.\n\n\u002F\u002F 3. Index-as-key is safe ONLY for: never-reordered, only-appended\u002Fremoved-from-end,\n\u002F\u002F    no-per-item-state lists (e.g., static nav). Everything else needs stable data keys.\n\n\u002F\u002F 4. Keys must be STABLE (same item → same key every render) and UNIQUE among siblings\n\u002F\u002F    (not globally). Source: database id, UUID. NEVER Math.random() or Date.now() per render.\n\n\u002F\u002F 5. Key goes on the OUTERMOST element from .map(). For multiple siblings per item,\n\u002F\u002F    use \u003CFragment key={id}> (not the \u003C> shorthand, which can't take a key).\n\n\u002F\u002F 6. Deliberately changing a key forces a full remount + state reset — useful for\n\u002F\u002F    resetting forms on prop change, actively harmful as a \"force update\" hammer.\n\n\u002F\u002F 7. For large lists (>100 items), use virtualization. Keys remain critical —\n\u002F\u002F    stable keys ensure scrolled-out items reuse (not remount) when they return to view.\n",[30,5411,5412,5417,5422,5427,5431,5436,5441,5445,5450,5455,5459,5464,5469,5473,5478,5483,5487,5492,5497,5501,5506],{"__ignoreMap":28},[33,5413,5414],{"class":35,"line":36},[33,5415,5416],{"class":39},"\u002F\u002F 1. Reconciliation diffs React element trees (not real DOM) using an O(n) heuristic:\n",[33,5418,5419],{"class":35,"line":43},[33,5420,5421],{"class":39},"\u002F\u002F    different types → full teardown; same type + same position → update in place.\n",[33,5423,5424],{"class":35,"line":49},[33,5425,5426],{"class":39},"\u002F\u002F    Keys correct the \"same position\" assumption for lists.\n",[33,5428,5429],{"class":35,"line":55},[33,5430,59],{"emptyLinePlaceholder":58},[33,5432,5433],{"class":35,"line":62},[33,5434,5435],{"class":39},"\u002F\u002F 2. Without keys, React matches array children POSITIONALLY — index-0 to index-0.\n",[33,5437,5438],{"class":35,"line":68},[33,5439,5440],{"class":39},"\u002F\u002F    This silently misattributes per-item DOM\u002Fstate on reorder, filter, or delete-from-middle.\n",[33,5442,5443],{"class":35,"line":74},[33,5444,59],{"emptyLinePlaceholder":58},[33,5446,5447],{"class":35,"line":79},[33,5448,5449],{"class":39},"\u002F\u002F 3. Index-as-key is safe ONLY for: never-reordered, only-appended\u002Fremoved-from-end,\n",[33,5451,5452],{"class":35,"line":85},[33,5453,5454],{"class":39},"\u002F\u002F    no-per-item-state lists (e.g., static nav). Everything else needs stable data keys.\n",[33,5456,5457],{"class":35,"line":91},[33,5458,59],{"emptyLinePlaceholder":58},[33,5460,5461],{"class":35,"line":97},[33,5462,5463],{"class":39},"\u002F\u002F 4. Keys must be STABLE (same item → same key every render) and UNIQUE among siblings\n",[33,5465,5466],{"class":35,"line":103},[33,5467,5468],{"class":39},"\u002F\u002F    (not globally). Source: database id, UUID. NEVER Math.random() or Date.now() per render.\n",[33,5470,5471],{"class":35,"line":109},[33,5472,59],{"emptyLinePlaceholder":58},[33,5474,5475],{"class":35,"line":114},[33,5476,5477],{"class":39},"\u002F\u002F 5. Key goes on the OUTERMOST element from .map(). For multiple siblings per item,\n",[33,5479,5480],{"class":35,"line":120},[33,5481,5482],{"class":39},"\u002F\u002F    use \u003CFragment key={id}> (not the \u003C> shorthand, which can't take a key).\n",[33,5484,5485],{"class":35,"line":126},[33,5486,59],{"emptyLinePlaceholder":58},[33,5488,5489],{"class":35,"line":132},[33,5490,5491],{"class":39},"\u002F\u002F 6. Deliberately changing a key forces a full remount + state reset — useful for\n",[33,5493,5494],{"class":35,"line":138},[33,5495,5496],{"class":39},"\u002F\u002F    resetting forms on prop change, actively harmful as a \"force update\" hammer.\n",[33,5498,5499],{"class":35,"line":143},[33,5500,59],{"emptyLinePlaceholder":58},[33,5502,5503],{"class":35,"line":149},[33,5504,5505],{"class":39},"\u002F\u002F 7. For large lists (>100 items), use virtualization. Keys remain critical —\n",[33,5507,5508],{"class":35,"line":155},[33,5509,5510],{"class":39},"\u002F\u002F    stable keys ensure scrolled-out items reuse (not remount) when they return to view.\n",[5512,5513,5514],"style",{},"html pre.shiki code .sdCPZ, html code.shiki .sdCPZ{--shiki-default:#6A737D;--shiki-github-dark:#6A737D}html pre.shiki code .svdQ7, html code.shiki .svdQ7{--shiki-default:#D73A49;--shiki-github-dark:#F97583}html pre.shiki code .sIsaT, html code.shiki .sIsaT{--shiki-default:#6F42C1;--shiki-github-dark:#B392F0}html pre.shiki code .ssxIu, html code.shiki .ssxIu{--shiki-default:#24292E;--shiki-github-dark:#E1E4E8}html pre.shiki code .sCrzJ, html code.shiki .sCrzJ{--shiki-default:#E36209;--shiki-github-dark:#FFAB70}html pre.shiki code .snvgF, html code.shiki .snvgF{--shiki-default:#005CC5;--shiki-github-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html.github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html pre.shiki code .sJ6F3, html code.shiki .sJ6F3{--shiki-default:#032F62;--shiki-github-dark:#9ECBFF}html pre.shiki code .sk71V, html code.shiki .sk71V{--shiki-default:#22863A;--shiki-github-dark:#85E89D}",{"title":28,"searchDepth":43,"depth":43,"links":5516},[5517,5518,5519,5520,5521,5522,5523,5525,5526,5527,5528,5529],{"id":16,"depth":43,"text":17},{"id":544,"depth":43,"text":545},{"id":869,"depth":43,"text":870},{"id":1316,"depth":43,"text":1317},{"id":1638,"depth":43,"text":1639},{"id":2441,"depth":43,"text":2442},{"id":3124,"depth":43,"text":5524},"Forcing a Remount with key",{"id":3468,"depth":43,"text":3469},{"id":4691,"depth":43,"text":4692},{"id":4822,"depth":43,"text":4823},{"id":4968,"depth":43,"text":4969},{"id":5402,"depth":43,"text":5403},"Reconciliation algorithm internals, key prop mechanics, why index keys corrupt state on reorder, virtual DOM diffing strategy, production patterns for dynamic lists. Code-first reference for mid-to-senior React engineers.","md",{},"\u002Freact\u002F13-lists-keys-and-reconciliation",{"title":5,"description":5530},"react\u002F13-lists-keys-and-reconciliation","WoAzR6g8mt506fvqVHhDYXHTc_Lb4GiamPqonijgYdY",1789924652260]