{"id":"l-rf-3-1","courseId":"react-fundamentals","lang":"en","kind":"lesson","summary":"Give components memory with useState, understand state as a per-render snapshot, use updater functions for successive updates, and replace arrays and objects instead of mutating them.","html":"<p>Click a “Mark as watched” button in a plain function component and nothing happens. Here’s the code most people write first:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"jsx\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">JSX</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> default</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> MovieItem</span><span style=\"color:#953800;--shiki-dark:#FFA657\">() </span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  let</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> watched </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> handleClick</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    watched </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> !</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">watched;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onClick</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">handleClick</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      {</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">watched </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Watched'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Mark as watched'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  );</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Two things go wrong. Changing a local variable doesn’t tell React anything happened, so it never renders again. And even if something else caused a render, <code dir=\"ltr\">let watched = false</code> would run again and reset the value, because a component is a function and its local variables start fresh on every call. A component needs two things: memory that survives between renders, and a way to ask React to render again. That’s state.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\"><code dir=\"ltr\">useState</code></h2>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"jsx\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">src/components/MovieItem.jsx</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { useState } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'react'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> default</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> MovieItem</span><span style=\"color:#953800;--shiki-dark:#FFA657\">({ title }) </span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">watched</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">setWatched</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">li</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      {</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">title</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-pressed</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">watched</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onClick</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> setWatched</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">watched)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">        {</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">watched </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Watched'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Mark as watched'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">li</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  );</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p><code dir=\"ltr\">useState(false)</code> declares one piece of state with an initial value of <code dir=\"ltr\">false</code>. It returns a pair, which you destructure and name: the current value, and a setter function. The <code dir=\"ltr\">[thing, setThing]</code> naming is a convention everyone follows; stick to it.</p>\n<p>When you call <code dir=\"ltr\">setWatched(true)</code>, React stores the new value and schedules a re-render. It calls <code dir=\"ltr\">MovieItem</code> again, and this time <code dir=\"ltr\">useState</code> returns <code dir=\"ltr\">true</code>. Your JSX is calculated from that, and React updates the DOM to match.</p>\n<p>State belongs to a component <strong>instance</strong>, not to the function. Render <code dir=\"ltr\">&lt;MovieItem /&gt;</code> three times and you get three independent <code dir=\"ltr\">watched</code> values. Clicking one doesn’t affect the others.</p>\n<aside class=\"callout callout-why\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg><span><span class=\"callout-kind\">Why this matters</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Functions starting with “use” are Hooks</span></p>\n<p><code dir=\"ltr\">useState</code> is a Hook: a special function that lets components use React features. Hooks rely on being called in the same order on every render, which is how React knows which state is which. So call them at the top level of your component: never inside an <code dir=\"ltr\">if</code>, a loop or a nested function. The linter in your Vite project checks the Rules of Hooks (Oxlint’s <code dir=\"ltr\">react/rules-of-hooks</code>, or <code dir=\"ltr\">eslint-plugin-react-hooks</code> if you chose ESLint) and will flag mistakes for you.</p>\n</aside>\n<h2 id=\"sec-2\" tabindex=\"-1\">Every render is a snapshot</h2>\n<p>This is the idea that makes state click. When React calls your component, it hands you the state <strong>for that render</strong>. Everything in that render, the JSX, the event handlers, all of it, sees those values. Setting state doesn’t change the variable you already have; it requests a new render with a new value.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 220\" role=\"img\" aria-labelledby=\"l-rf-3-1-t1\"><defs><marker id=\"arrow-l-rf-3-1-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-rf-3-1-t1\">Render 1 sees watched equals false. A click calls setWatched(true), which schedules render 2. Render 2 sees watched equals true, and React commits the changed DOM.</title>\n  <rect class=\"d-box\" x=\"20\" y=\"60\" width=\"170\" height=\"80\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"105\" y=\"92\" text-anchor=\"middle\">Render 1</text>\n  <text class=\"d-code\" x=\"105\" y=\"118\" text-anchor=\"middle\">watched = false</text>\n  <rect class=\"d-box-warn\" x=\"265\" y=\"60\" width=\"170\" height=\"80\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"350\" y=\"92\" text-anchor=\"middle\">Click</text>\n  <text class=\"d-code\" x=\"350\" y=\"118\" text-anchor=\"middle\">setWatched(true)</text>\n  <rect class=\"d-box-success\" x=\"510\" y=\"60\" width=\"170\" height=\"80\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"595\" y=\"92\" text-anchor=\"middle\">Render 2</text>\n  <text class=\"d-code\" x=\"595\" y=\"118\" text-anchor=\"middle\">watched = true</text>\n  <path class=\"d-arrow\" d=\"M190 100 L263 100\" marker-end=\"url(#arrow-l-rf-3-1-1)\"/>\n  <path class=\"d-arrow\" d=\"M435 100 L508 100\" marker-end=\"url(#arrow-l-rf-3-1-1)\"/>\n  <text class=\"d-label-muted\" x=\"227\" y=\"175\" text-anchor=\"middle\">user acts</text>\n  <text class=\"d-label-muted\" x=\"472\" y=\"175\" text-anchor=\"middle\">React re-renders</text>\n  <text class=\"d-label-muted\" x=\"350\" y=\"205\" text-anchor=\"middle\">Each render keeps its own values; nothing changes mid-render</text>\n</svg><figcaption>Setting state triggers a new render with a new snapshot</figcaption></figure>\n<p>That’s why this surprises people:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"jsx\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">JSX</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">count</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">setCount</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> addThree</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() {</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  setCount</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(count </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// count is 0 → asks for 1</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  setCount</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(count </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// count is still 0 → asks for 1</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  setCount</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(count </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// still 0 → 1</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>All three calls read the same snapshot, so the result is 1, not 3. When the next value depends on the previous one, pass an <strong>updater function</strong> instead of a value:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"jsx\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">JSX</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> addThree</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() {</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  setCount</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">c</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> c </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// 0 → 1</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  setCount</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">c</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> c </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// 1 → 2</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  setCount</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">c</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> c </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// 2 → 3</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>React queues the updaters and runs them in order, each receiving the result of the one before. You don’t need updaters everywhere; <code dir=\"ltr\">setWatched(!watched)</code> in a click handler is fine. Use them when you set the same state more than once in a handler, or in code that runs later, such as a timer.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Arrays and objects: replace, don’t mutate</h2>\n<p>The Watchlist keeps an array of movie objects in state. React decides whether state changed by comparing the old and new value with <code dir=\"ltr\">Object.is</code>. If you change an array in place and pass the same array back, React sees the same reference and may skip the render entirely.</p>\n<p>So treat state as read-only and build new values:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"jsx\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">JSX</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">movies</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">setMovies</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">INITIAL_MOVIES</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Add: a new array with everything plus one more</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setMovies</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">([</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">movies, { id: crypto.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">randomUUID</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(), title: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Dune'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, watched: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }]);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Remove: a new array without one item</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setMovies</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(movies.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">filter</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">m</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> m.id </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!==</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> id));</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Update one: a new array, with a new object for the changed item</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setMovies</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(movies.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">map</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">m</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (m.id </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> id </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">m, watched: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">m.watched } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> m)));</span></span></code></pre></div>\n<p>These three lines (spread to add, <code dir=\"ltr\">filter</code> to remove, <code dir=\"ltr\">map</code> with a spread to update) cover most of the state updates you’ll ever write for lists. Objects work the same way: <code dir=\"ltr\">setFilters({ ...filters, year: 2024 })</code>.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Mutating state</span></p>\n<p><code dir=\"ltr\">movies.push(newMovie); setMovies(movies);</code> changes the array React is holding and hands it back unchanged as far as <code dir=\"ltr\">Object.is</code> is concerned. The screen doesn’t update, or updates later at a random moment when something else re-renders. If you see stale UI after an update, look for <code dir=\"ltr\">push</code>, <code dir=\"ltr\">splice</code>, <code dir=\"ltr\">sort</code> or a direct assignment like <code dir=\"ltr\">movie.watched = true</code>.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">Choosing the initial value</h2>\n<p>The argument to <code dir=\"ltr\">useState</code> is only used on the first render. Later renders ignore it. If computing the initial value is expensive, such as reading and parsing saved data, pass a function instead, <code dir=\"ltr\">useState(() =&gt; loadMovies())</code>, and React calls it only on the first render instead of on every render. You’ll use exactly that to load the Watchlist from localStorage in Section 4.</p>\n<p>Now your components can remember things. Next, you’ll look closely at the events that change state: click, keyboard and the event object itself.</p>\n","toc":[{"id":"sec-1","text":"useState","level":2},{"id":"sec-2","text":"Every render is a snapshot","level":2},{"id":"sec-3","text":"Arrays and objects: replace, don’t mutate","level":2},{"id":"sec-4","text":"Choosing the initial value","level":2}],"takeaways":["<code dir=\"ltr\">useState</code> returns the current value and a setter; calling the setter schedules a re-render with the new value.","Each render sees a fixed snapshot of state, so reading state right after setting it still gives the old value.","Use an updater function, <code dir=\"ltr\">setCount((c) =&gt; c + 1)</code>, when the next value depends on the previous one.","Never mutate state; create new arrays and objects with spread, <code dir=\"ltr\">map</code> and <code dir=\"ltr\">filter</code> so React notices the change.","Call Hooks at the top level of a component, never inside conditions, loops or nested functions."],"further":[{"title":"State: A Component's Memory","url":"https://react.dev/learn/state-a-components-memory"},{"title":"State as a Snapshot","url":"https://react.dev/learn/state-as-a-snapshot"},{"title":"Updating Arrays in State","url":"https://react.dev/learn/updating-arrays-in-state"},{"title":"useState reference","url":"https://react.dev/reference/react/useState"}],"quiz":[{"q":"<p><code dir=\"ltr\">count</code> starts at 0. What does the button show after one click?</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"jsx\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">JSX</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> handleClick</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() {</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  setCount</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(count </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  setCount</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(count </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  setCount</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(count </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n","options":[{"text":"3","why":"Each call reads <code dir=\"ltr\">count</code> from the same render, where it’s 0. All three calls ask for 0 + 1."},{"text":"0","why":"The state does change. It just changes to 1, not 3."},{"text":"An error, because you can only call a setter once per handler.","why":"You can call setters as often as you like. React batches them and re-renders once."},{"text":"1","why":"Correct. <code dir=\"ltr\">count</code> is a snapshot of 0 for the whole handler, so all three calls set 1. Use <code dir=\"ltr\">setCount((c) =&gt; c + 1)</code> to stack updates."}],"answer":3},{"q":"<p>Why doesn’t this toggle work? <code dir=\"ltr\">let watched = false;</code> inside the component, and the button’s handler does <code dir=\"ltr\">watched = !watched;</code>.</p>","options":[{"text":"Booleans can’t be toggled with <code dir=\"ltr\">!</code> in React.","why":"The <code dir=\"ltr\">!</code> operator works fine. The problem is where the value lives and what triggers a render."},{"text":"The handler needs to return the new value.","why":"Event handlers’ return values are ignored. Something has to tell React to render again."},{"text":"Changing a local variable doesn’t trigger a re-render, and the next render would reset it to false anyway.","why":"Correct. Local variables don’t persist between renders and React doesn’t watch them. <code dir=\"ltr\">useState</code> solves both problems."},{"text":"<code dir=\"ltr\">let</code> should be <code dir=\"ltr\">var</code>.","why":"The keyword doesn’t matter. Any local variable is recreated on every render."}],"answer":2},{"q":"<p>Which correctly marks one movie as watched in <code dir=\"ltr\">movies</code> state?</p>","options":[{"text":"<code dir=\"ltr\">setMovies(movies.map((m) =&gt; (m.id === id ? { ...m, watched: true } : m)));</code>","why":"Correct. <code dir=\"ltr\">map</code> builds a new array, and the changed movie is a new object. Unchanged movies are reused."},{"text":"<code dir=\"ltr\">movies.find((m) =&gt; m.id === id).watched = true; setMovies(movies);</code>","why":"This mutates the existing object and passes the same array back. React compares with Object.is, sees no change and may skip the render."},{"text":"<code dir=\"ltr\">setMovies(movies.push({ id, watched: true }));</code>","why":"<code dir=\"ltr\">push</code> mutates the array and returns the new length, so state becomes a number."},{"text":"<code dir=\"ltr\">movies[0].watched = true;</code>","why":"Mutating state never tells React anything changed, and it edits the wrong movie unless it happens to be first."}],"answer":0},{"q":"<p>Inside a click handler you call <code dir=\"ltr\">setTitle('Dune')</code> and then <code dir=\"ltr\">console.log(title)</code> on the next line. The old title was ‘Arrival’. What’s logged?</p>","options":[{"text":"‘Dune’","why":"Setting state doesn’t change the variable in the current render. It asks React for a new render where <code dir=\"ltr\">title</code> will be ‘Dune’."},{"text":"‘Arrival’","why":"Correct. <code dir=\"ltr\">title</code> is a constant in this render’s snapshot. The new value appears in the next render."},{"text":"undefined","why":"The variable still holds this render’s value, which is ‘Arrival’."}],"answer":1}],"exercise":{"kind":"web","mode":"react","starter":{"jsx":"import { useState } from 'react';\n\nexport default function App() {\n  let watched = false;\n  const [rating, setRating] = useState(0);\n\n  function toggleWatched() {\n    watched = !watched;\n  }\n\n  function addOne() {\n    setRating(rating + 1);\n  }\n\n  function addTwo() {\n    setRating(rating + 1);\n    setRating(rating + 1);\n  }\n\n  return (\n    <article>\n      <h2>Arrival</h2>\n      <button id=\"toggle\" aria-pressed={watched} onClick={toggleWatched}>\n        {watched ? 'Watched' : 'Mark as watched'}\n      </button>\n      <p id=\"rating\">Rating: {rating}/5</p>\n      <button id=\"plus\" onClick={addOne}>+1</button>\n      <button id=\"plus-two\" onClick={addTwo}>+2</button>\n    </article>\n  );\n}\n"},"solution":{"jsx":"import { useState } from 'react';\n\nexport default function App() {\n  const [watched, setWatched] = useState(false);\n  const [rating, setRating] = useState(0);\n\n  function toggleWatched() {\n    setWatched(!watched);\n  }\n\n  function addOne() {\n    setRating((r) => Math.min(r + 1, 5));\n  }\n\n  function addTwo() {\n    addOne();\n    addOne();\n  }\n\n  return (\n    <article>\n      <h2>Arrival</h2>\n      <button id=\"toggle\" aria-pressed={watched} onClick={toggleWatched}>\n        {watched ? 'Watched' : 'Mark as watched'}\n      </button>\n      <p id=\"rating\">Rating: {rating}/5</p>\n      <button id=\"plus\" onClick={addOne}>+1</button>\n      <button id=\"plus-two\" onClick={addTwo}>+2</button>\n    </article>\n  );\n}\n"},"checks":[{"id":"toggle-on","test":"await click('#toggle'); return text('#toggle') === 'Watched' && $('#toggle').getAttribute('aria-pressed') === 'true';"},{"id":"toggle-off","test":"await click('#toggle'); return text('#toggle') === 'Mark as watched' && $('#toggle').getAttribute('aria-pressed') === 'false';"},{"id":"plus-two","test":"await click('#plus-two'); return text('#rating') === 'Rating: 2/5';"},{"id":"max-five","test":"for (let i = 0; i < 5; i++) await click('#plus'); return text('#rating') === 'Rating: 5/5';"}],"title":"Watched toggle and a rating","prompt":"<p>This movie card has two broken features.</p>\n<ol>\n<li><strong>Mark as watched</strong> (<code dir=\"ltr\">#toggle</code>) does nothing. Make it toggle between <strong>Mark as watched</strong> and <strong>Watched</strong>, with <code dir=\"ltr\">aria-pressed</code> following along.</li>\n<li>The rating: <strong>+2</strong> (<code dir=\"ltr\">#plus-two</code>) only adds 1, and the rating can climb past 5. Make +2 add two, and never let the rating go above 5, whichever button is clicked.</li>\n</ol>\n","hints":["A <code dir=\"ltr\">let</code> variable is reset on every render and never triggers one. Store <code dir=\"ltr\">watched</code> with <code dir=\"ltr\">useState(false)</code>.","Both calls in <code dir=\"ltr\">addTwo</code> read the same <code dir=\"ltr\">rating</code> snapshot. Use an updater: <code dir=\"ltr\">setRating((r) =&gt; …)</code>.","Clamp inside the updater: <code dir=\"ltr\">setRating((r) =&gt; Math.min(r + 1, 5))</code>. Then <code dir=\"ltr\">addTwo</code> can call <code dir=\"ltr\">addOne()</code> twice."],"explanation":"<p><code dir=\"ltr\">watched</code> has to be state: a local variable neither survives the next render nor causes one. For the rating, each handler sees a snapshot, so <code dir=\"ltr\">setRating(rating + 1)</code> twice asks for the same value twice. An updater function receives the latest queued value instead, and putting <code dir=\"ltr\">Math.min(…, 5)</code> inside it applies the limit to every update, including both halves of +2.</p>\n","checkLabels":{"toggle-on":"Clicking the toggle shows “Watched” and sets aria-pressed to true","toggle-off":"Clicking again shows “Mark as watched” and aria-pressed false","plus-two":"+2 raises the rating from 0 to 2","max-five":"The rating stops at 5"}},"words":698,"readMinutes":3}