[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"playground-react-weekly-derived-state-bug":44,"search-suggestions":58},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"slug":45,"title":46,"summary":47,"html":48,"css":49,"js":50,"config":51,"doc":53},"react-weekly-derived-state-bug","React derived state — interactive playground",null,"\u003Cdiv class=\"pg\">\n    \u003Ch1 class=\"pg-title\">🎮 Derived state, live\u003C\u002Fh1>\n    \u003Cp class=\"pg-sub\">\n      Click a filter or \"Complete all.\" In \u003Cb>Buggy\u003C\u002Fb> mode the filtered list is its own\n      \u003Ccode>useState\u003C\u002Fcode>, kept in sync by a real \u003Ccode>useEffect\u003C\u002Fcode> — watch the render\n      log count two real commits for one click. Flip to \u003Cb>Fixed\u003C\u002Fb> and the same click\n      produces exactly one, because the list is just a formula now.\n    \u003C\u002Fp>\n\n    \u003Cdiv class=\"banner info show\" id=\"boot\">Loading the real React 19.3.0 runtime from the CDN…\u003C\u002Fdiv>\n\n    \u003Cdiv id=\"app\" hidden>\n      \u003Cdiv class=\"panel\">\n        \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">1\u003C\u002Fspan> Mode\u003C\u002Fdiv>\n        \u003Clabel class=\"toggle\">\n          \u003Cinput type=\"checkbox\" id=\"buggyToggle\" checked \u002F>\n          \u003Cspan class=\"switch\">\u003C\u002Fspan>\n          \u003Cspan>Buggy: sync \u003Ccode>visibleTodos\u003C\u002Fcode> with a \u003Ccode>useEffect\u003C\u002Fcode>\u003C\u002Fspan>\n        \u003C\u002Flabel>\n        \u003Cp class=\"hint\" id=\"modeHint\" style=\"margin-top:8px\">\u003C\u002Fp>\n\n        \u003Cdiv class=\"seg\" id=\"filterSeg\" style=\"margin-top:14px\">\n          \u003Cbutton data-filter=\"all\">All\u003C\u002Fbutton>\n          \u003Cbutton data-filter=\"active\">Active\u003C\u002Fbutton>\n          \u003Cbutton data-filter=\"done\">Done\u003C\u002Fbutton>\n        \u003C\u002Fdiv>\n        \u003Cdiv class=\"presets\" style=\"margin-top:12px\">\n          \u003Cbutton class=\"btn ghost sm\" id=\"completeAllBtn\">Complete all (bulk update)\u003C\u002Fbutton>\n          \u003Cbutton class=\"btn ghost sm\" id=\"resetBtn\">Reset demo\u003C\u002Fbutton>\n        \u003C\u002Fdiv>\n      \u003C\u002Fdiv>\n\n      \u003Cdiv class=\"panel\" style=\"margin-top:16px\">\n        \u003Cdiv class=\"panel-label\">\n          \u003Cspan class=\"n\">2\u003C\u002Fspan> Todo list\n          \u003Cspan class=\"render-badge\" id=\"renderBadge\">renders: 0\u003C\u002Fspan>\n        \u003C\u002Fdiv>\n        \u003Cdiv id=\"list\">\u003C\u002Fdiv>\n        \u003Cdiv class=\"stale-banner\" id=\"staleBanner\">\n          ⚠️ showing results for the \u003Cem>previous\u003C\u002Fem> filter — the sync Effect hasn't caught up yet\n        \u003C\u002Fdiv>\n      \u003C\u002Fdiv>\n\n      \u003Cdiv class=\"panel\" style=\"margin-top:16px\">\n        \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">3\u003C\u002Fspan> What React actually did\u003C\u002Fdiv>\n        \u003Cp class=\"hint\" style=\"margin-top:0\">\n          Every line comes from a real \u003Ccode>useEffect\u003C\u002Fcode> with no dependency array, which\n          React runs after \u003Cem>every\u003C\u002Fem> commit — this counts real renders, not a description of them.\n        \u003C\u002Fp>\n        \u003Cdiv class=\"log\" id=\"log\" aria-live=\"polite\">\u003C\u002Fdiv>\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n  \u003C\u002Fdiv>","\u002F*\n * Shared playground design system — daily-post\n * ============================================\n * Link it from any post's playground\u002Findex.html (no per-post boilerplate):\n *\n *   \u003Clink rel=\"stylesheet\" href=\"..\u002F..\u002F..\u002Fplayground\u002Fassets\u002Fbase.css\" \u002F>\n *\n * The relative path resolves the same locally (posts\u002F\u003Cbase>\u002Fplayground\u002F) and on\n * the deployed host (public\u002Fposts\u002F\u003Cbase>\u002Fplayground\u002F), because build-site.js\n * mirrors this file to public\u002Fplayground\u002Fassets\u002Fbase.css. For a CodeSandbox\n * --embed, playground.js inlines this file so the single-file sandbox still styles.\n *\n * Then write ONLY the demo-specific CSS in your inline \u003Cstyle>. Everything below\n * — tokens, layout, panels, buttons, toggles, sliders, code blocks, status\u002Flog,\n * banners, presets — comes for free and looks consistent across every post.\n *\n * Theme-aware: dark by default, light via prefers-color-scheme AND a\n * [data-theme] override on \u003Chtml> (so a toggle can force either). Accessible:\n * real focus rings, adequate contrast, reduced-motion honored.\n *\u002F\n\n\u002F* ---------- design tokens ---------- *\u002F\n:root {\n  --bg: #0d1117;\n  --bg-grid: #ffffff0a;\n  --panel: #161b22;\n  --panel-2: #0b0e14;\n  --card: #0b0e14;\n  --ink: #e8ebf2;\n  --muted: #9aa4b2;\n  --faint: #6b7688;\n  --line: #232a35;\n  --line-soft: #ffffff12;\n  --accent: #7c9cff;\n  --accent-ink: #0b0e14;\n  --good: #3fb950;\n  --warn: #f0883e;\n  --bad: #f85149;\n  --info: #58a6ff;\n  --radius: 14px;\n  --radius-sm: 9px;\n  --shadow: 0 8px 30px #0006;\n  --shadow-sm: 0 4px 14px #0005;\n  --mono: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n  --sans: ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", sans-serif;\n  color-scheme: dark;\n}\n\n\u002F* Light theme — system preference, overridable by [data-theme] on \u003Chtml>. *\u002F\n@media (prefers-color-scheme: light) {\n  :root:not([data-theme=\"dark\"]) {\n    --bg: #f4f6fb;\n    --bg-grid: #00000008;\n    --panel: #ffffff;\n    --panel-2: #f0f3f9;\n    --card: #f7f9fc;\n    --ink: #131722;\n    --muted: #5b6472;\n    --faint: #8a93a3;\n    --line: #e2e7f0;\n    --line-soft: #00000010;\n    --accent: #2f6df0;\n    --accent-ink: #ffffff;\n    --good: #1a7f37;\n    --warn: #bc4c00;\n    --bad: #cf222e;\n    --info: #0969da;\n    --shadow: 0 8px 30px #0000001f;\n    --shadow-sm: 0 4px 14px #00000014;\n    color-scheme: light;\n  }\n}\n:root[data-theme=\"light\"] {\n  --bg: #f4f6fb;\n  --bg-grid: #00000008;\n  --panel: #ffffff;\n  --panel-2: #f0f3f9;\n  --card: #f7f9fc;\n  --ink: #131722;\n  --muted: #5b6472;\n  --faint: #8a93a3;\n  --line: #e2e7f0;\n  --line-soft: #00000010;\n  --accent: #2f6df0;\n  --accent-ink: #ffffff;\n  --good: #1a7f37;\n  --warn: #bc4c00;\n  --bad: #cf222e;\n  --info: #0969da;\n  --shadow: 0 8px 30px #0000001f;\n  --shadow-sm: 0 4px 14px #00000014;\n  color-scheme: light;\n}\n\n\u002F* ---------- reset & base ---------- *\u002F\n*, *::before, *::after { box-sizing: border-box; }\n\nbody {\n  margin: 0;\n  min-height: 100vh;\n  font-family: var(--sans);\n  color: var(--ink);\n  background:\n    radial-gradient(circle at 1px 1px, var(--bg-grid) 1px, transparent 0) 0 0 \u002F 24px 24px,\n    var(--bg);\n  display: flex;\n  justify-content: center;\n  align-items: flex-start;\n  padding: clamp(14px, 3vw, 26px);\n  line-height: 1.55;\n  -webkit-font-smoothing: antialiased;\n  text-rendering: optimizeLegibility;\n}\n\n\u002F* The single content column. *\u002F\n.pg {\n  width: 100%;\n  max-width: 860px;\n}\n\n\u002F* ---------- header ---------- *\u002F\n.pg-title {\n  font-size: clamp(1.15rem, 1rem + 1vw, 1.4rem);\n  font-weight: 800;\n  letter-spacing: -0.02em;\n  margin: 0 0 4px;\n  display: flex;\n  align-items: center;\n  gap: 9px;\n}\n.pg-sub {\n  color: var(--muted);\n  font-size: 0.92rem;\n  line-height: 1.5;\n  margin: 0 0 16px;\n  max-width: 66ch;\n}\n.pg-sub strong, .pg-sub b { color: var(--ink); font-weight: 700; }\n.pg-sub code { font-family: var(--mono); background: var(--line-soft); color: var(--accent);\n  padding: 1px 6px; border-radius: 6px; font-size: 0.85em; }\n\n\u002F* ---------- panels ---------- *\u002F\n.panel {\n  background: var(--panel);\n  border: 1px solid var(--line);\n  border-radius: var(--radius);\n  padding: 16px 18px;\n  margin-bottom: 14px;\n  box-shadow: var(--shadow-sm);\n}\n.panel-label {\n  font-size: 0.7rem;\n  text-transform: uppercase;\n  letter-spacing: 0.08em;\n  color: var(--muted);\n  font-weight: 800;\n  margin: 0 0 13px;\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n.panel-label .n {\n  display: inline-grid; place-items: center;\n  width: 18px; height: 18px; border-radius: 50%;\n  background: var(--accent); color: var(--accent-ink);\n  font-size: 0.66rem; font-weight: 800;\n}\n\n\u002F* ---------- layout helpers ---------- *\u002F\n.row { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }\n.row.between { justify-content: space-between; }\n.col { display: flex; flex-direction: column; gap: 10px; }\n.grow { flex: 1 1 0; min-width: 0; }\n.wrap-controls { display: flex; flex-wrap: wrap; gap: 16px 24px; align-items: flex-end; }\n.field { display: flex; flex-direction: column; gap: 7px; min-width: 0; }\n.field > .label, .ctl-label {\n  font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em;\n  color: var(--muted); font-weight: 800;\n}\n\n\u002F* ---------- buttons ---------- *\u002F\n.btn {\n  font: inherit; font-weight: 650; font-size: 0.88rem;\n  cursor: pointer; user-select: none;\n  border: 1px solid var(--line);\n  background: var(--card); color: var(--ink);\n  padding: 9px 15px; border-radius: var(--radius-sm);\n  display: inline-flex; align-items: center; gap: 8px;\n  transition: background .15s ease, border-color .15s ease, transform .05s ease, color .15s ease;\n}\n.btn:hover { background: var(--line-soft); border-color: var(--muted); }\n.btn:active { transform: translateY(1px); }\n.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }\n.btn[disabled] { opacity: 0.45; cursor: not-allowed; }\n.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }\n.btn.primary:hover { filter: brightness(1.08); background: var(--accent); }\n.btn.good { background: var(--good); border-color: var(--good); color: #04140a; }\n.btn.bad  { background: var(--bad);  border-color: var(--bad);  color: #fff; }\n.btn.ghost { background: transparent; }\n.btn.sm { padding: 6px 11px; font-size: 0.82rem; }\n\n.btn-row { display: flex; flex-wrap: wrap; gap: 9px; }\n\n\u002F* Segmented control — a row of buttons where one is active (aria-pressed). *\u002F\n.seg { display: inline-flex; background: var(--card); border: 1px solid var(--line);\n  border-radius: var(--radius-sm); padding: 3px; gap: 3px; }\n.seg button {\n  font: inherit; font-weight: 650; font-size: 0.84rem; cursor: pointer;\n  border: none; background: transparent; color: var(--muted);\n  padding: 7px 13px; border-radius: 7px;\n  transition: background .15s ease, color .15s ease;\n}\n.seg button:hover { color: var(--ink); }\n.seg button[aria-pressed=\"true\"] { background: var(--accent); color: var(--accent-ink); }\n.seg button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }\n\n\u002F* Preset chips — worked-example shortcuts. *\u002F\n.presets { display: flex; flex-wrap: wrap; gap: 8px; }\n.presets .btn { background: var(--panel-2); }\n\n\u002F* ---------- toggle switch ---------- *\u002F\n.toggle { display: inline-flex; align-items: center; gap: 10px; cursor: pointer;\n  font-size: 0.88rem; font-weight: 600; user-select: none; }\n.toggle input { position: absolute; opacity: 0; width: 0; height: 0; }\n.switch { position: relative; width: 44px; height: 25px; border-radius: 999px; flex: none;\n  background: var(--line); border: 1px solid var(--line); transition: background .18s ease; }\n.switch::after { content: \"\"; position: absolute; top: 2px; left: 2px; width: 19px; height: 19px;\n  border-radius: 50%; background: #cfd6e4; transition: transform .18s ease; box-shadow: 0 1px 3px #0006; }\n.toggle input:checked + .switch { background: var(--good); border-color: var(--good); }\n.toggle input:checked + .switch::after { transform: translateX(19px); background: #fff; }\n.toggle input:focus-visible + .switch { outline: 2px solid var(--accent); outline-offset: 2px; }\n\n\u002F* ---------- range slider ---------- *\u002F\ninput[type=\"range\"] {\n  -webkit-appearance: none; appearance: none; width: 100%; height: 6px;\n  background: var(--line); border-radius: 999px; outline: none; cursor: pointer; margin: 8px 0;\n}\ninput[type=\"range\"]::-webkit-slider-thumb {\n  -webkit-appearance: none; appearance: none; width: 19px; height: 19px; border-radius: 50%;\n  background: var(--accent); border: 2px solid var(--panel); box-shadow: var(--shadow-sm); cursor: pointer;\n}\ninput[type=\"range\"]::-moz-range-thumb {\n  width: 19px; height: 19px; border-radius: 50%; background: var(--accent);\n  border: 2px solid var(--panel); box-shadow: var(--shadow-sm); cursor: pointer;\n}\ninput[type=\"range\"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }\n\n\u002F* ---------- text inputs \u002F selects ---------- *\u002F\ninput[type=\"text\"], input[type=\"number\"], input[type=\"search\"], select, textarea {\n  font: inherit; color: var(--ink); background: var(--card);\n  border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 8px 11px; width: 100%;\n}\ntextarea { font-family: var(--mono); font-size: 0.85rem; line-height: 1.6; resize: vertical; }\ninput:focus-visible, select:focus-visible, textarea:focus-visible {\n  outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent);\n}\n\n\u002F* ---------- status \u002F output \u002F log ---------- *\u002F\n.status {\n  display: flex; align-items: center; gap: 9px; flex-wrap: wrap;\n  font-size: 0.9rem; font-weight: 600;\n  padding: 11px 14px; border-radius: var(--radius-sm);\n  background: var(--panel-2); border: 1px solid var(--line); color: var(--ink);\n}\n.status.good { border-color: color-mix(in srgb, var(--good) 55%, var(--line)); }\n.status.warn { border-color: color-mix(in srgb, var(--warn) 55%, var(--line)); }\n.status.bad  { border-color: color-mix(in srgb, var(--bad) 55%, var(--line)); }\n.dot { width: 9px; height: 9px; border-radius: 50%; background: var(--muted); flex: none; }\n.dot.good { background: var(--good); } .dot.warn { background: var(--warn); }\n.dot.bad { background: var(--bad); } .dot.info { background: var(--info); }\n\n.output { background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius-sm);\n  padding: 13px 15px; }\n.log { font-family: var(--mono); font-size: 0.8rem; line-height: 1.7; background: var(--card);\n  border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 11px 14px;\n  max-height: 220px; overflow-y: auto; white-space: pre-wrap; }\n\n\u002F* ---------- code block + syntax tokens ---------- *\u002F\n.code {\n  font-family: var(--mono); font-size: 0.82rem; line-height: 1.7;\n  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius-sm);\n  padding: 13px 15px; overflow-x: auto; white-space: pre; tab-size: 2;\n}\n.code .tok-sel  { color: var(--accent); }\n.code .tok-prop { color: #86e29b; }\n.code .tok-kw   { color: #ff9bcb; }\n.code .tok-str  { color: #f0c674; }\n.code .tok-num  { color: #f0883e; }\n.code .tok-cmt  { color: var(--faint); font-style: italic; }\n.code .hl { background: color-mix(in srgb, var(--good) 22%, transparent); border-radius: 4px; padding: 0 3px; }\n\n\u002F* ---------- hint \u002F caption ---------- *\u002F\n.hint { font-size: 0.83rem; color: var(--muted); line-height: 1.55; margin: 10px 0 0; }\n.hint code { font-family: var(--mono); color: var(--ink); background: var(--line-soft);\n  padding: 1px 5px; border-radius: 5px; font-size: 0.86em; }\n.hint strong { color: var(--ink); }\n\n\u002F* ---------- banner (feature-support \u002F notices) ---------- *\u002F\n.banner {\n  display: none; margin-bottom: 14px; padding: 11px 14px; border-radius: 11px;\n  font-size: 0.85rem; line-height: 1.5;\n  color: var(--ink); background: color-mix(in srgb, var(--warn) 12%, var(--panel));\n  border: 1px solid color-mix(in srgb, var(--warn) 45%, var(--line));\n}\n.banner.show { display: block; }\n.banner.info { background: color-mix(in srgb, var(--info) 12%, var(--panel));\n  border-color: color-mix(in srgb, var(--info) 45%, var(--line)); }\n.banner code { font-family: var(--mono); background: var(--line-soft); padding: 1px 5px; border-radius: 5px; }\n\n\u002F* ---------- misc atoms ---------- *\u002F\n.pill { display: inline-flex; align-items: center; gap: 6px; font-size: 0.75rem; font-weight: 700;\n  padding: 3px 9px; border-radius: 999px; background: var(--line-soft); color: var(--muted); }\n.pill.good { color: var(--good); background: color-mix(in srgb, var(--good) 16%, transparent); }\n.pill.bad  { color: var(--bad);  background: color-mix(in srgb, var(--bad) 16%, transparent); }\n.mono { font-family: var(--mono); }\n.muted { color: var(--muted); }\n.big-num { font-family: var(--mono); font-size: 1.6rem; font-weight: 800; letter-spacing: -0.02em; }\n\n\u002F* Optional theme toggle button (top-right). Wire it in JS if you want it. *\u002F\n.theme-btn { position: fixed; top: 12px; right: 12px; z-index: 20; }\n\n\u002F* ---------- responsive ---------- *\u002F\n@media (max-width: 560px) {\n  .panel { padding: 14px; }\n  .wrap-controls { gap: 14px; }\n  .btn { padding: 9px 13px; }\n}\n\n\u002F* ---------- reduced motion ---------- *\u002F\n@media (prefers-reduced-motion: reduce) {\n  *, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }\n}\n\n\n\u002F* Demo-specific only — base.css owns tokens, panels, buttons, status, log. *\u002F\n  .todo-list { list-style: none; margin: 12px 0 0; padding: 0; display: grid; gap: 8px; }\n  .todo {\n    display: flex; align-items: center; gap: 10px;\n    border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 8px 12px;\n    background: var(--panel-2);\n  }\n  .todo.done .text { text-decoration: line-through; color: var(--muted); }\n  .todo .text { flex: 1; }\n  .stale-banner {\n    margin-top: 10px; padding: 8px 12px; border-radius: var(--radius-sm);\n    background: color-mix(in srgb, #e5484d 18%, var(--panel-2));\n    border: 1px solid color-mix(in srgb, #e5484d 45%, var(--line));\n    font-size: .85rem; display: none;\n  }\n  .stale-banner.show { display: block; }\n  .render-badge {\n    font-variant-numeric: tabular-nums; font-size: .72rem; padding: 2px 8px;\n    border-radius: 999px; background: var(--bg); border: 1px solid var(--line); color: var(--muted);\n    margin-left: 8px;\n  }","const $ = (id) => document.getElementById(id);\n\nlet React, createRoot;\ntry {\n  \u002F\u002F Pinned to the exact version the article verifies.\n  [React, { createRoot }] = await Promise.all([\n    import(\"https:\u002F\u002Fesm.sh\u002Freact@19.3.0\"),\n    import(\"https:\u002F\u002Fesm.sh\u002Freact-dom@19.3.0\u002Fclient?deps=react@19.3.0\"),\n  ]);\n} catch (err) {\n  $(\"boot\").className = \"banner show\";\n  $(\"boot\").innerHTML =\n    \"\u003Cb>React couldn't load from the CDN\u003C\u002Fb> — you're offline, or a network policy blocked esm.sh. \" +\n    \"This demo deliberately runs the \u003Cem>real\u003C\u002Fem> React reconciler rather than imitating it, so \" +\n    \"there's nothing honest to show without it. The article's code samples stand on their own; \" +\n    \"try again with a connection.\";\n  throw err;\n}\n\nconst { createElement: h, useState, useEffect } = React;\n\n$(\"boot\").hidden = true;\n$(\"app\").hidden = false;\n\nconst INITIAL_TODOS = [\n  { id: \"a\", text: \"Write the RFC\", done: false },\n  { id: \"b\", text: \"Review Bilal's PR\", done: false },\n  { id: \"c\", text: \"Renew the domain\", done: true },\n  { id: \"d\", text: \"Reply to the support ticket\", done: false },\n  { id: \"e\", text: \"Update the changelog\", done: true },\n];\n\nfunction computeVisible(todos, filter) {\n  if (filter === \"active\") return todos.filter((t) => !t.done);\n  if (filter === \"done\") return todos.filter((t) => t.done);\n  return todos;\n}\n\nconst logEl = $(\"log\");\nfunction logLine(text, cls = \"\") {\n  const el = document.createElement(\"div\");\n  el.className = cls;\n  el.textContent = text;\n  logEl.prepend(el);\n  while (logEl.childElementCount > 40) logEl.lastElementChild.remove();\n}\n\nlet commitCount = 0;\n\u002F\u002F Set by the child List component's own effect, read by App's effect right\n\u002F\u002F after (children's effects always fire before their parent's, for the\n\u002F\u002F same commit) — so the log always describes the commit that just happened.\nlet lastCommitWasStale = false;\n\n\u002F\u002F Buggy: visibleTodos is its own state, kept in sync by an Effect that\n\u002F\u002F runs AFTER the commit — so one filter click produces two real renders.\nfunction BuggyList({ todos, filter }) {\n  const [visibleTodos, setVisibleTodos] = useState(() => computeVisible(todos, filter));\n\n  useEffect(() => {\n    setVisibleTodos(computeVisible(todos, filter));\n  }, [todos, filter]);\n\n  const expected = computeVisible(todos, filter);\n  const stale = JSON.stringify(visibleTodos) !== JSON.stringify(expected);\n\n  useEffect(() => {\n    lastCommitWasStale = stale;\n    $(\"staleBanner\").classList.toggle(\"show\", stale);\n  });\n\n  return h(TodoUl, { items: visibleTodos });\n}\n\nfunction TodoUl({ items }) {\n  return h(\"ul\", { className: \"todo-list\" },\n    items.map((t) =>\n      h(\"li\", { key: t.id, className: \"todo\" + (t.done ? \" done\" : \"\") },\n        h(\"span\", { className: \"text\" }, t.text)\n      )\n    )\n  );\n}\n\n\u002F\u002F Fixed: no second state, no Effect — visibleTodos is a plain calculation\n\u002F\u002F during render, so one filter click produces exactly one render.\nfunction FixedList({ todos, filter }) {\n  const visibleTodos = computeVisible(todos, filter);\n  useEffect(() => {\n    lastCommitWasStale = false;\n    $(\"staleBanner\").classList.remove(\"show\");\n  });\n  return h(TodoUl, { items: visibleTodos });\n}\n\nfunction App() {\n  const [todos, setTodos] = useState(INITIAL_TODOS);\n  const [filter, setFilter] = useState(\"all\");\n  const [buggy, setBuggy] = useState(true);\n\n  \u002F\u002F No dependency array: React runs this after every real commit, buggy\n  \u002F\u002F or fixed. It runs AFTER the List component's own effect above (child\n  \u002F\u002F effects fire before the parent's for the same commit), so\n  \u002F\u002F lastCommitWasStale already describes the commit that just happened.\n  useEffect(() => {\n    commitCount++;\n    $(\"renderBadge\").textContent = `renders: ${commitCount}`;\n    logLine(\n      lastCommitWasStale\n        ? `🔄 commit #${commitCount} — corrected by the sync Effect`\n        : `✅ commit #${commitCount} — direct render`,\n      lastCommitWasStale ? \"\" : \"good\"\n    );\n  });\n\n  useEffect(() => {\n    $(\"buggyToggle\").onchange = (e) => setBuggy(e.target.checked);\n    $(\"modeHint\").textContent = buggy\n      ? \"Buggy: visibleTodos is its own useState, synced by a useEffect after every commit.\"\n      : \"Fixed: visibleTodos is a const computed inline — nothing to keep in sync.\";\n    document.querySelectorAll(\"#filterSeg button\").forEach((btn) => {\n      btn.setAttribute(\"aria-pressed\", String(btn.dataset.filter === filter));\n      btn.onclick = () => setFilter(btn.dataset.filter);\n    });\n    $(\"completeAllBtn\").onclick = () => {\n      setTodos((prev) => prev.map((t) => ({ ...t, done: true })));\n    };\n  });\n\n  const List = buggy ? BuggyList : FixedList;\n  return h(List, { todos, filter });\n}\n\nconst root = createRoot($(\"list\"));\n\n$(\"resetBtn\").addEventListener(\"click\", () => {\n  logEl.innerHTML = \"\";\n  commitCount = 0;\n  $(\"renderBadge\").textContent = \"renders: 0\";\n  root.render(h(App, { key: \"reset-\" + Date.now() }));\n});\n\nroot.render(h(App));",{"height":52,"autorun":17},520,{"slug":45,"title":54,"excerpt":55,"coverUrl":56,"vertical":57},"React Derived State: Why That useState Is Probably a Bug","React derived state explained: why syncing a computed value into useState with an Effect causes extra renders and drift, and what to write instead.","\u002Fmedia\u002Fcovers\u002Freact-weekly-derived-state-bug.png",{"slug":6},[59,63,67,71,75,79,83,87,91,95,98,102],{"slug":60,"name":61,"articles":62},"webdev","Webdev",103,{"slug":64,"name":65,"articles":66},"javascript","Javascript",87,{"slug":68,"name":69,"articles":70},"frontend","Frontend",71,{"slug":72,"name":73,"articles":74},"css","Css",34,{"slug":76,"name":77,"articles":78},"tutorial","Tutorial",32,{"slug":80,"name":81,"articles":82},"typescript","Typescript",15,{"slug":84,"name":85,"articles":86},"react","React",13,{"slug":88,"name":89,"articles":90},"performance","Performance",12,{"slug":92,"name":93,"articles":94},"browser","Browser",9,{"slug":96,"name":97,"articles":94},"node","Node",{"slug":99,"name":100,"articles":101},"html","Html",6,{"slug":103,"name":104,"articles":101},"grammar","Grammar"]