[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"playground-vue-weekly-reactivity-ref-vs-reactive":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},"vue-weekly-reactivity-ref-vs-reactive","Vue reactivity: ref vs reactive — interactive playground",null,"\u003Cdiv class=\"pg\">\n    \u003Ch1 class=\"pg-title\">🎮 Vue reactivity: watch the subscription\u003C\u002Fh1>\n    \u003Cp class=\"pg-sub\">\n      Four counters. Two update the screen, two change their data and leave the screen alone.\n      Click the \u003Cb>+1\u003C\u002Fb> buttons and compare the two numbers in each row: \u003Cb>screen\u003C\u002Fb> is what\n      a render wrote, \u003Cb>data\u003C\u002Fb> is what the value actually is right now. Where they drift apart,\n      Vue was never subscribed to the thing you changed.\n    \u003C\u002Fp>\n\n    \u003Cdiv class=\"banner info\" id=\"boot\">Loading the real Vue reactivity runtime (3.5.41) from the CDN…\u003C\u002Fdiv>\n\n    \u003Cdiv class=\"panel\" id=\"app\" hidden>\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">1\u003C\u002Fspan> Four counters\u003C\u002Fdiv>\n      \u003Cdiv class=\"rows\">\n\n        \u003Cdiv class=\"row alive\" id=\"row-a\">\n          \u003Cdiv>\n            \u003Ch3>\u003Cspan class=\"pill\">A\u003C\u002Fspan> \u003Ccode>ref(0)\u003C\u002Fcode> → \u003Ccode>a.value++\u003C\u002Fcode>\u003C\u002Fh3>\n            \u003Cp class=\"why\">The value lives behind a property, so reading it is a property access — trackable.\u003C\u002Fp>\n          \u003C\u002Fdiv>\n          \u003Cdiv class=\"nums\">\n            \u003Cdiv class=\"num screen\">\u003Cb data-screen=\"a\">0\u003C\u002Fb>\u003Cspan>screen\u003C\u002Fspan>\u003C\u002Fdiv>\n            \u003Cdiv class=\"num\">\u003Cb data-data=\"a\">0\u003C\u002Fb>\u003Cspan>data\u003C\u002Fspan>\u003C\u002Fdiv>\n            \u003Cdiv class=\"num\">\u003Cb data-renders=\"a\">0\u003C\u002Fb>\u003Cspan>renders\u003C\u002Fspan>\u003C\u002Fdiv>\n          \u003C\u002Fdiv>\n          \u003Cdiv class=\"verdict\">\u003Cspan class=\"status good\">\u003Cspan class=\"dot\">\u003C\u002Fspan>\u003Cspan data-verdict=\"a\">in sync\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n\n        \u003Cdiv class=\"row alive\" id=\"row-b\">\n          \u003Cdiv>\n            \u003Ch3>\u003Cspan class=\"pill\">B\u003C\u002Fspan> \u003Ccode>reactive({count:0})\u003C\u002Fcode> → \u003Ccode>b.count++\u003C\u002Fcode>\u003C\u002Fh3>\n            \u003Cp class=\"why\">A property access on the proxy, read inside the effect. Subscription created, then written to.\u003C\u002Fp>\n          \u003C\u002Fdiv>\n          \u003Cdiv class=\"nums\">\n            \u003Cdiv class=\"num screen\">\u003Cb data-screen=\"b\">0\u003C\u002Fb>\u003Cspan>screen\u003C\u002Fspan>\u003C\u002Fdiv>\n            \u003Cdiv class=\"num\">\u003Cb data-data=\"b\">0\u003C\u002Fb>\u003Cspan>data\u003C\u002Fspan>\u003C\u002Fdiv>\n            \u003Cdiv class=\"num\">\u003Cb data-renders=\"b\">0\u003C\u002Fb>\u003Cspan>renders\u003C\u002Fspan>\u003C\u002Fdiv>\n          \u003C\u002Fdiv>\n          \u003Cdiv class=\"verdict\">\u003Cspan class=\"status good\">\u003Cspan class=\"dot\">\u003C\u002Fspan>\u003Cspan data-verdict=\"b\">in sync\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n\n        \u003Cdiv class=\"row dead\" id=\"row-c\">\n          \u003Cdiv>\n            \u003Ch3>\u003Cspan class=\"pill\">C\u003C\u002Fspan> \u003Ccode>let { count } = reactive(…)\u003C\u002Fcode> → \u003Ccode>count++\u003C\u002Fcode>\u003C\u002Fh3>\n            \u003Cp class=\"why\">Destructuring copied the number out during setup — outside any effect. There was never a subscription to create.\u003C\u002Fp>\n          \u003C\u002Fdiv>\n          \u003Cdiv class=\"nums\">\n            \u003Cdiv class=\"num screen stale\">\u003Cb data-screen=\"c\">0\u003C\u002Fb>\u003Cspan>screen\u003C\u002Fspan>\u003C\u002Fdiv>\n            \u003Cdiv class=\"num\">\u003Cb data-data=\"c\">0\u003C\u002Fb>\u003Cspan>data\u003C\u002Fspan>\u003C\u002Fdiv>\n            \u003Cdiv class=\"num\">\u003Cb data-renders=\"c\">0\u003C\u002Fb>\u003Cspan>renders\u003C\u002Fspan>\u003C\u002Fdiv>\n          \u003C\u002Fdiv>\n          \u003Cdiv class=\"verdict\">\u003Cspan class=\"status bad\">\u003Cspan class=\"dot\">\u003C\u002Fspan>\u003Cspan data-verdict=\"c\">data moved, screen didn't\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n\n        \u003Cdiv class=\"row dead\" id=\"row-d\">\n          \u003Cdiv>\n            \u003Ch3>\u003Cspan class=\"pill\">D\u003C\u002Fspan> \u003Ccode>d = { count: d.count + 1 }\u003C\u002Fcode>\u003C\u002Fh3>\n            \u003Cp class=\"why\">The effect subscribed to \u003Ccode>count\u003C\u002Fcode> \u003Cem>on that proxy\u003C\u002Fem>. Reassigning the variable leaves every subscriber pointed at the old object.\u003C\u002Fp>\n          \u003C\u002Fdiv>\n          \u003Cdiv class=\"nums\">\n            \u003Cdiv class=\"num screen stale\">\u003Cb data-screen=\"d\">0\u003C\u002Fb>\u003Cspan>screen\u003C\u002Fspan>\u003C\u002Fdiv>\n            \u003Cdiv class=\"num\">\u003Cb data-data=\"d\">0\u003C\u002Fb>\u003Cspan>data\u003C\u002Fspan>\u003C\u002Fdiv>\n            \u003Cdiv class=\"num\">\u003Cb data-renders=\"d\">0\u003C\u002Fb>\u003Cspan>renders\u003C\u002Fspan>\u003C\u002Fdiv>\n          \u003C\u002Fdiv>\n          \u003Cdiv class=\"verdict\">\u003Cspan class=\"status bad\">\u003Cspan class=\"dot\">\u003C\u002Fspan>\u003Cspan data-verdict=\"d\">data moved, screen didn't\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n\n      \u003C\u002Fdiv>\n\n      \u003Cdiv class=\"btn-row\" style=\"margin-top:14px\">\n        \u003Cbutton class=\"btn primary\" data-inc=\"a\">+1 A\u003C\u002Fbutton>\n        \u003Cbutton class=\"btn primary\" data-inc=\"b\">+1 B\u003C\u002Fbutton>\n        \u003Cbutton class=\"btn\" data-inc=\"c\">+1 C\u003C\u002Fbutton>\n        \u003Cbutton class=\"btn\" data-inc=\"d\">+1 D\u003C\u002Fbutton>\n        \u003Cbutton class=\"btn ghost\" id=\"reset\">Reset\u003C\u002Fbutton>\n      \u003C\u002Fdiv>\n\n      \u003Cdiv class=\"presets\" style=\"margin-top:14px\">\n        \u003Cbutton class=\"btn sm\" id=\"all\">Click all four once\u003C\u002Fbutton>\n        \u003Clabel class=\"toggle\" style=\"margin-left:6px\">\n          \u003Cinput type=\"checkbox\" id=\"fix\" \u002F>\n          \u003Cspan class=\"switch\">\u003C\u002Fspan>\n          \u003Cspan>Fix C and D (\u003Ccode>toRefs\u003C\u002Fcode> \u002F \u003Ccode>ref\u003C\u002Fcode>)\u003C\u002Fspan>\n        \u003C\u002Flabel>\n      \u003C\u002Fdiv>\n\n      \u003Cp class=\"hint\" id=\"fixhint\" style=\"margin-top:10px\">\n        Flip the switch to rebuild C with \u003Ccode>toRefs(state)\u003C\u002Fcode> and D as a \u003Ccode>ref\u003C\u002Fcode> holding the\n        object. Same clicks, same arithmetic — the difference is only \u003Cem>where the write lands\u003C\u002Fem>.\n      \u003C\u002Fp>\n    \u003C\u002Fdiv>\n\n    \u003Cdiv class=\"panel\" id=\"logpanel\" hidden>\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">2\u003C\u002Fspan> What re-ran, and why\u003C\u002Fdiv>\n      \u003Cp class=\"hint\" style=\"margin-top:0\">\n        Every line is one real \u003Ccode>watchEffect\u003C\u002Fcode> re-run — Vue deciding, on its own, that an\n        effect's dependency changed. Notice C and D never appear until you flip the fix.\n      \u003C\u002Fp>\n      \u003Cdiv class=\"log\" id=\"log\" aria-live=\"polite\">\u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\n    \u003Cdiv class=\"panel\" id=\"codepanel\" hidden>\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">3\u003C\u002Fspan> The rule, in one sentence\u003C\u002Fdiv>\n      \u003Cdiv class=\"code\">Reactivity is a \u003Cspan class=\"hl\">subscription between one property and one effect\u003C\u002Fspan>,\ncreated when the effect \u003Cspan class=\"tok-kw\">reads\u003C\u002Fspan> the property.\n\n\u003Cspan class=\"tok-cmt\">\u002F\u002F no read inside an effect → no subscription\u003C\u002Fspan>\n\u003Cspan class=\"tok-cmt\">\u002F\u002F wrote to a different object → nobody was listening\u003C\u002Fspan>\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  .rows { display: grid; gap: 10px; }\n  .row {\n    display: grid; grid-template-columns: 1fr auto; gap: 12px 14px; align-items: center;\n    padding: 12px 14px; border: 1px solid var(--line); border-radius: var(--radius-sm);\n    background: var(--panel-2);\n  }\n  .row.dead { border-color: color-mix(in oklab, #f87171 45%, var(--line)); }\n  .row.alive { border-color: color-mix(in oklab, #34d399 40%, var(--line)); }\n  .row h3 { font-size: .95rem; margin: 0 0 2px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }\n  .row .why { font-size: .8rem; line-height: 1.45; color: var(--muted); margin: 0; }\n  .nums { display: flex; gap: 10px; align-items: stretch; text-align: center; }\n  .num { min-width: 92px; padding: 8px 10px; border-radius: var(--radius-sm); background: var(--bg); border: 1px solid var(--line); }\n  .num b { display: block; font-size: 1.5rem; line-height: 1.1; font-variant-numeric: tabular-nums; }\n  .num span { display: block; font-size: .66rem; letter-spacing: .5px; text-transform: uppercase; color: var(--muted); }\n  .num.screen b { color: var(--accent); }\n  .num.stale b { color: #f87171; }\n  .verdict { grid-column: 1 \u002F -1; font-size: .82rem; padding-top: 2px; }\n  @media (max-width: 620px) { .row { grid-template-columns: 1fr; } .nums { justify-content: flex-start; } }","const $ = (s, r = document) => r.querySelector(s);\nconst $$ = (s, r = document) => [...r.querySelectorAll(s)];\n\nlet vue;\ntry {\n  \u002F\u002F Pinned to the exact version the article documents.\n  vue = await import(\"https:\u002F\u002Fesm.sh\u002Fvue@3.5.41\");\n} catch (err) {\n  $(\"#boot\").className = \"banner\";\n  $(\"#boot\").innerHTML =\n    \"\u003Cb>Vue 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> Vue runtime rather than imitating it, so there's \" +\n    \"nothing honest to show without it. The article's four counters stand on their own; try again with a connection.\";\n  throw err;\n}\n\nconst { ref, reactive, watchEffect, toRefs } = vue;\n\n$(\"#boot\").hidden = true;\nfor (const id of [\"#app\", \"#logpanel\", \"#codepanel\"]) $(id).hidden = false;\n\nconst log = $(\"#log\");\nconst renders = { a: 0, b: 0, c: 0, d: 0 };\nlet stops = [];\nlet fixed = false;\nlet booting = true;\n\n\u002F\u002F The four pieces of state, wired the four different ways.\nlet A, B, Craw, Cref, Dproxy, Dref;\n\nfunction line(text, cls = \"\") {\n  const el = document.createElement(\"div\");\n  el.className = cls;\n  el.textContent = text;\n  log.prepend(el);\n  while (log.childElementCount > 40) log.lastElementChild.remove();\n}\n\nfunction paintScreen(key, value) {\n  $(`[data-screen=\"${key}\"]`).textContent = String(value);\n  renders[key]++;\n  $(`[data-renders=\"${key}\"]`).textContent = String(renders[key]);\n  \u002F\u002F A re-render is exactly when the verdict can change. Effects flush on a\n  \u002F\u002F microtask, not synchronously, so the click handler's own verdict check ran\n  \u002F\u002F while the screen was still showing the old number — re-check it here.\n  syncVerdict(key);\n  if (!booting) line(`▶ ${key.toUpperCase()} re-rendered → ${value}`, \"good\");\n}\n\nfunction paintData(key, value) {\n  $(`[data-data=\"${key}\"]`).textContent = String(value);\n  syncVerdict(key);\n}\n\nfunction syncVerdict(key) {\n  const screen = $(`[data-screen=\"${key}\"]`).textContent;\n  const data = $(`[data-data=\"${key}\"]`).textContent;\n  const ok = screen === data;\n  const row = $(`#row-${key}`);\n  const status = $(`.verdict .status`, row);\n  const label = $(`[data-verdict=\"${key}\"]`, row);\n  row.classList.toggle(\"alive\", ok);\n  row.classList.toggle(\"dead\", !ok);\n  status.className = `status ${ok ? \"good\" : \"bad\"}`;\n  label.textContent = ok ? \"in sync\" : \"data moved, screen didn't\";\n  $(`.num.screen`, row).classList.toggle(\"stale\", !ok);\n}\n\n\u002F** (Re)build all four counters and their render effects from scratch. *\u002F\nfunction build() {\n  stops.forEach((stop) => stop());\n  stops = [];\n  booting = true;\n  for (const k of [\"a\", \"b\", \"c\", \"d\"]) renders[k] = 0;\n\n  \u002F\u002F A — ref\n  A = ref(0);\n  \u002F\u002F B — reactive, mutated\n  B = reactive({ count: 0 });\n  \u002F\u002F C — destructured out of a reactive object (broken), or via toRefs (fixed)\n  const cSource = reactive({ count: 0 });\n  if (fixed) {\n    ({ count: Cref } = toRefs(cSource));\n    Craw = null;\n  } else {\n    ({ count: Craw } = cSource); \u002F\u002F a plain number, copied out\n    Cref = null;\n  }\n  \u002F\u002F D — a proxy that gets replaced (broken), or a ref holding the object (fixed)\n  Dproxy = reactive({ count: 0 });\n  Dref = fixed ? ref({ count: 0 }) : null;\n\n  \u002F\u002F Each of these is a \"render\": an effect that reads state and writes the DOM.\n  stops.push(watchEffect(() => paintScreen(\"a\", A.value)));\n  stops.push(watchEffect(() => paintScreen(\"b\", B.count)));\n  stops.push(\n    watchEffect(() => paintScreen(\"c\", fixed ? Cref.value : Craw))\n  );\n  stops.push(\n    watchEffect(() => paintScreen(\"d\", fixed ? Dref.value.count : Dproxy.count))\n  );\n\n  for (const k of [\"a\", \"b\", \"c\", \"d\"]) paintData(k, 0);\n  booting = false;\n  line(\n    fixed\n      ? \"— rebuilt: C uses toRefs(state), D holds the object in a ref —\"\n      : \"— rebuilt: C destructures, D reassigns —\"\n  );\n}\n\nconst dataValue = {\n  a: () => A.value,\n  b: () => B.count,\n  c: () => (fixed ? Cref.value : Craw),\n  d: () => (fixed ? Dref.value.count : Dproxy.count),\n};\n\nfunction inc(key) {\n  if (key === \"a\") A.value++;\n  if (key === \"b\") B.count++;\n  if (key === \"c\") fixed ? Cref.value++ : Craw++;          \u002F\u002F ❌ writes a local number when unfixed\n  if (key === \"d\") {\n    if (fixed) Dref.value = { count: Dref.value.count + 1 }; \u002F\u002F ✅ writes `value`, which is tracked\n    else Dproxy = { count: Dproxy.count + 1 };               \u002F\u002F ❌ points the variable at a new object\n  }\n  paintData(key, dataValue[key]());\n  if (!fixed && (key === \"c\" || key === \"d\")) {\n    line(`✗ ${key.toUpperCase()} data is now ${dataValue[key]()} — no effect re-ran`, \"bad\");\n  }\n}\n\n$$(\"[data-inc]\").forEach((b) => b.addEventListener(\"click\", () => inc(b.dataset.inc)));\n$(\"#all\").addEventListener(\"click\", () => [\"a\", \"b\", \"c\", \"d\"].forEach(inc));\n$(\"#reset\").addEventListener(\"click\", () => { log.textContent = \"\"; build(); });\n$(\"#fix\").addEventListener(\"change\", (e) => { fixed = e.target.checked; log.textContent = \"\"; build(); });\n\nbuild();",{"height":52,"autorun":17},520,{"slug":45,"title":54,"excerpt":55,"coverUrl":56,"vertical":57},"Vue Reactivity Explained: ref vs reactive (+ Cheat Sheet)","A complete guide to Vue reactivity: how ref and reactive really work, why destructuring breaks updates, when to use each, and a copy-paste cheat sheet.","\u002Fmedia\u002Fcovers\u002Fvue-weekly-reactivity-ref-vs-reactive.png",{"slug":6},[59,63,67,71,75,79,83,87,90,94,98,101],{"slug":60,"name":61,"articles":62},"webdev","Webdev",65,{"slug":64,"name":65,"articles":66},"frontend","Frontend",57,{"slug":68,"name":69,"articles":70},"javascript","Javascript",55,{"slug":72,"name":73,"articles":74},"css","Css",24,{"slug":76,"name":77,"articles":78},"typescript","Typescript",10,{"slug":80,"name":81,"articles":82},"performance","Performance",9,{"slug":84,"name":85,"articles":86},"tutorial","Tutorial",6,{"slug":88,"name":89,"articles":86},"grammar","Grammar",{"slug":91,"name":92,"articles":93},"react","React",5,{"slug":95,"name":96,"articles":97},"node","Node",4,{"slug":99,"name":100,"articles":97},"html","Html",{"slug":102,"name":103,"articles":43},"ai","Ai"]