{"id":"l-rf-3-5","courseId":"react-fundamentals","lang":"en","kind":"lesson","summary":"Use useEffect to keep React state in sync with systems outside React, write correct dependencies and cleanups, and recognise the many cases where you don’t need an Effect at all.","html":"<p>So far, everything in your components has been about React: props in, JSX out, state changes triggering renders. But the Watchlist also needs to touch things React doesn’t manage. The browser tab should say “2 to watch”, and the list should survive a page refresh by being saved to localStorage. Neither of those is rendering. They’re <strong>synchronization</strong>: keeping something outside React in step with your state. That’s what Effects are for.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Your first Effect</h2>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"jsx\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">src/App.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\"> { useEffect, 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\"> App</span><span style=\"color:#953800;--shiki-dark:#FFA657\">() </span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> left</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</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:#CF222E;--shiki-dark:#FF7B72\"> !</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">m.watched).</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">length</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  useEffect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    document.title </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">left</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} to watch · Watchlist`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }, [left]);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">  // …render the app</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p><code dir=\"ltr\">useEffect</code> takes a function and a <strong>dependency array</strong>. React renders your component, updates the screen, and then runs the function. After later renders, it runs the function again only if something in the dependency array changed since last time. Mark a movie watched, <code dir=\"ltr\">left</code> goes from 2 to 1, and the title updates. Type in the add form, <code dir=\"ltr\">left</code> doesn’t change, and the Effect is skipped.</p>\n<p>The dependency array has three forms:</p>\n<div class=\"table-wrap\" role=\"region\" aria-label=\"Scrollable table\" tabindex=\"0\"><table><thead>\n<tr>\n<th>You write</th>\n<th>The Effect runs</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code dir=\"ltr\">useEffect(fn, [a, b])</code></td>\n<td>After the first render and whenever <code dir=\"ltr\">a</code> or <code dir=\"ltr\">b</code> changes</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">useEffect(fn, [])</code></td>\n<td>After the first render only</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">useEffect(fn)</code></td>\n<td>After every render</td>\n</tr>\n</tbody>\n</table></div><p>The rule for what goes in the array is not a choice: <strong>list every value from the component that the Effect reads</strong>, such as props, state, and variables derived from them. The exhaustive-deps lint rule in your Vite project (<code dir=\"ltr\">react/exhaustive-deps</code> in Oxlint, <code dir=\"ltr\">react-hooks/exhaustive-deps</code> in ESLint) checks this for you. If you leave something out, the Effect keeps using an old value, which is the bug in the third quiz question.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Cleanup</h2>\n<p>Some Effects start something that must be stopped: a timer, an event listener, a connection. Return a function that undoes it:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"jsx\" data-index=\"1\"><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:#8250DF;--shiki-dark:#D2A8FF\">useEffect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> handleKeyDown</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">e</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (e.key </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '/'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">focusSearch</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\">  window.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">addEventListener</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'keydown'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, handleKeyDown);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> window.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">removeEventListener</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'keydown'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, handleKeyDown);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}, []);</span></span></code></pre></div>\n<p>React calls the cleanup before running the Effect again and when the component is removed from the screen. Without it, every mount would add another listener, and pressing “/” would fire several times.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 250\" role=\"img\" aria-labelledby=\"l-rf-3-5-t1\"><defs><marker id=\"arrow-l-rf-3-5-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-5-t1\">After the first render and commit, the Effect runs. When a dependency changes, React re-renders, runs the previous cleanup, then runs the Effect again. When the component unmounts, React runs the cleanup one last time.</title>\n  <rect class=\"d-box\" x=\"10\" y=\"70\" width=\"140\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"80\" y=\"103\" text-anchor=\"middle\">Render + commit</text>\n  <rect class=\"d-box-primary\" x=\"190\" y=\"70\" width=\"120\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"250\" y=\"103\" text-anchor=\"middle\">Effect</text>\n  <rect class=\"d-box\" x=\"350\" y=\"70\" width=\"150\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"425\" y=\"96\" text-anchor=\"middle\">Deps changed:</text>\n  <text class=\"d-label\" x=\"425\" y=\"116\" text-anchor=\"middle\">re-render</text>\n  <rect class=\"d-box-warn\" x=\"540\" y=\"70\" width=\"150\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"615\" y=\"96\" text-anchor=\"middle\">Cleanup, then</text>\n  <text class=\"d-label\" x=\"615\" y=\"116\" text-anchor=\"middle\">Effect again</text>\n  <path class=\"d-arrow\" d=\"M150 98 L188 98\" marker-end=\"url(#arrow-l-rf-3-5-1)\"/>\n  <path class=\"d-arrow\" d=\"M310 98 L348 98\" marker-end=\"url(#arrow-l-rf-3-5-1)\"/>\n  <path class=\"d-arrow\" d=\"M500 98 L538 98\" marker-end=\"url(#arrow-l-rf-3-5-1)\"/>\n  <path class=\"d-arrow d-dashed\" d=\"M615 126 C 615 190, 425 190, 425 128\" marker-end=\"url(#arrow-l-rf-3-5-1)\"/>\n  <rect class=\"d-box-warn\" x=\"190\" y=\"170\" width=\"160\" height=\"50\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"270\" y=\"200\" text-anchor=\"middle\">Unmount: cleanup</text>\n  <path class=\"d-arrow\" d=\"M250 126 L260 168\" marker-end=\"url(#arrow-l-rf-3-5-1)\"/>\n  <text class=\"d-label-muted\" x=\"560\" y=\"210\" text-anchor=\"middle\">repeats on each change</text>\n</svg><figcaption>An Effect’s lifecycle</figcaption></figure>\n<h3 id=\"sec-3\" tabindex=\"-1\">Why it runs twice in development</h3>\n<p>With <code dir=\"ltr\">&lt;StrictMode&gt;</code> (which your <code dir=\"ltr\">main.jsx</code> has), React does something odd in development: it mounts your component, runs the Effects, immediately runs their cleanups as if unmounting, and then runs the Effects again. It’s a stress test. If your cleanup correctly undoes the Effect, the app behaves the same, and you see nothing but an extra log line. If it doesn’t, for example you forgot to remove a listener, you’ll notice now instead of in production. Don’t try to prevent the double run; fix the cleanup.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Saving the Watchlist to localStorage</h2>\n<p>Persisting state is the textbook Effect: whenever <code dir=\"ltr\">movies</code> changes, write it to storage.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"jsx\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">src/App.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:#0550AE;--shiki-dark:#79C0FF\"> STORAGE_KEY</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'watchlist.movies'</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\"> loadMovies</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  try</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> JSON</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">parse</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(localStorage.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getItem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">STORAGE_KEY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">??</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [];</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">catch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></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\">  }</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\">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\"> App</span><span style=\"color:#953800;--shiki-dark:#FFA657\">() </span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#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\">(loadMovies);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  useEffect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    localStorage.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setItem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">STORAGE_KEY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">JSON</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">stringify</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(movies));</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }, [movies]);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">  // …</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Two details matter. Loading happens in the <strong>initializer</strong>, <code dir=\"ltr\">useState(loadMovies)</code>, not in an Effect. Passing the function (not calling it) means React reads storage only for the first render, not on every render, so the saved list appears immediately. Loading in an Effect would render an empty list first, and the save Effect could overwrite your stored movies with that empty array. And <code dir=\"ltr\">loadMovies</code> uses <code dir=\"ltr\">try</code>/<code dir=\"ltr\">catch</code>, because storage can be blocked or contain corrupted data, and a broken value shouldn’t crash the whole app.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">You might not need an Effect</h2>\n<p>Effects are an escape hatch, and the most common Effect bugs come from using one where it isn’t needed. Before writing one, check these three cases.</p>\n<p><strong>Calculating something from props or state.</strong> Filtered lists, counts and formatted strings are calculated during render. Storing them in state and syncing with an Effect adds an extra render with stale data and more state that can drift.</p>\n<p><strong>Responding to a user action.</strong> If code should run because the user clicked Add, put it in the submit handler. An Effect that watches <code dir=\"ltr\">movies</code> can’t tell an add from a remove or a reload.</p>\n<p><strong>Resetting state when a prop changes.</strong> Instead of an Effect that clears a draft when <code dir=\"ltr\">movieId</code> changes, give the component a key: <code dir=\"ltr\">&lt;NotesEditor key={movieId} /&gt;</code>. A new key means a fresh component with fresh state.</p>\n<p>What’s left are true synchronizations: browser APIs, storage, timers, subscriptions and network connections. That’s a short list, and it’s the right one.</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>The infinite loop</span></p>\n<p>An Effect that sets state which is also in its own dependency array, <code dir=\"ltr\">useEffect(() =&gt; setCount(count + 1), [count])</code>, re-runs after every update, forever. The same happens when an Effect that sets state lists an object or array created during render as a dependency, because that’s a new value on every render. If the page freezes or React reports “Maximum update depth exceeded”, look for an Effect feeding its own dependencies.</p>\n</aside>\n<aside class=\"callout callout-note\" 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\"/><path d=\"M12 16v-4\"/><path d=\"M12 8h.01\"/></svg><span><span class=\"callout-kind\">Note</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Fetching data</span></p>\n<p>You can fetch data in an Effect, but doing it correctly means handling race conditions, caching and loading states yourself. Real projects usually use a framework or a library such as TanStack Query instead. You’ll see the options in the last lesson.</p>\n</aside>\n<p>That completes the core of React: components, props, state, events and Effects. In the next section you’ll put every piece together into the finished Watchlist, style it, and ship it.</p>\n","toc":[{"id":"sec-1","text":"Your first Effect","level":2},{"id":"sec-2","text":"Cleanup","level":2},{"id":"sec-3","text":"Why it runs twice in development","level":3},{"id":"sec-4","text":"Saving the Watchlist to localStorage","level":2},{"id":"sec-5","text":"You might not need an Effect","level":2}],"takeaways":["An Effect runs after React updates the screen and is for syncing with something outside React, such as the document title, localStorage, timers or subscriptions.","The dependency array lists every value from the component the Effect reads; React re-runs the Effect when one of them changes.","Return a cleanup function to undo what the Effect did; React runs it before the next run and on unmount.","In development, StrictMode runs every Effect, its cleanup, and the Effect again, to prove the cleanup works.","If a value can be calculated from props or state, or a change is caused by a specific user action, you don’t need an Effect."],"further":[{"title":"Synchronizing with Effects","url":"https://react.dev/learn/synchronizing-with-effects"},{"title":"You Might Not Need an Effect","url":"https://react.dev/learn/you-might-not-need-an-effect"},{"title":"useEffect reference","url":"https://react.dev/reference/react/useEffect"}],"quiz":[{"q":"<p><code dir=\"ltr\">visibleMovies</code> is kept in state and updated with <code dir=\"ltr\">useEffect(() =&gt; setVisibleMovies(movies.filter(…)), [movies, filter])</code>. What’s the better approach?</p>","options":[{"text":"Calculate it during render: <code dir=\"ltr\">const visibleMovies = movies.filter(…)</code>.","why":"Correct. It’s derived data. The Effect version renders once with stale data, then again with the right data, and adds state that can drift."},{"text":"Add <code dir=\"ltr\">visibleMovies</code> to the dependency array too.","why":"That makes the Effect re-run whenever it sets its own output, inviting a loop. The real fix is to remove the Effect."},{"text":"Move the filter into a <code dir=\"ltr\">setTimeout</code> inside the Effect.","why":"Delaying the work makes the stale render last longer. Nothing about this needs an Effect."}],"answer":0},{"q":"<p>In development, an Effect that logs “subscribed” and has a cleanup that logs “unsubscribed” prints “subscribed, unsubscribed, subscribed” on page load. What’s going on?</p>","options":[{"text":"The dependency array is missing, so the Effect runs on every render.","why":"Without an array the Effect re-runs after every re-render, but a plain page load renders once, so that alone can’t explain a cleanup straight after the first run."},{"text":"The component is rendered twice because of a bug in the parent.","why":"This specific sequence is deliberate, and it only happens in development."},{"text":"React 19 always runs Effects twice, in production too.","why":"The extra cycle is a development-only check from StrictMode. Production runs the Effect once."},{"text":"StrictMode mounts, unmounts and remounts the component in development to check that the cleanup fully undoes the Effect.","why":"Correct. If your app behaves the same after that cycle, your cleanup is right. If it breaks, the bug would have shown up in production eventually."}],"answer":3},{"q":"<p>What’s wrong with this Effect?</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:#8250DF;--shiki-dark:#D2A8FF\">useEffect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  document.title </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">left</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} to watch`</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":"<code dir=\"ltr\">document.title</code> can’t be set from React.","why":"Setting the document title is a classic, valid use of an Effect; it’s a browser API outside React."},{"text":"It reads <code dir=\"ltr\">left</code> but doesn’t list it, so the title is set once and never updates when <code dir=\"ltr\">left</code> changes.","why":"Correct. An empty array means “run after the first render only”. List every value the Effect reads, <code dir=\"ltr\">[left]</code>, and the lint rule will remind you."},{"text":"It needs a cleanup function or React throws.","why":"Cleanup is optional. Many Effects, like this one, have nothing to undo."},{"text":"Template literals don’t work inside Effects.","why":"An Effect is a normal function; any JavaScript works inside it."}],"answer":1},{"q":"<p>After adding a movie, you want to show a “Movie added” message. Where should the code that shows it go?</p>","options":[{"text":"In an Effect that watches <code dir=\"ltr\">movies</code> and shows the message whenever it changes.","why":"The list also changes when you remove, toggle or load movies, so the message would appear at the wrong times."},{"text":"In an Effect with an empty dependency array.","why":"That runs once after the first render, long before anyone adds a movie."},{"text":"In the submit handler, right where the movie is added.","why":"Correct. The message is caused by a specific user action, so it belongs in that action’s handler, not in an Effect."}],"answer":2}],"exercise":{"kind":"web","mode":"react","starter":{"jsx":"import { useEffect, useState } from 'react';\n\n// The page's own title, before the Watchlist changes it.\ndocument.title = 'Playground';\n\nconst INITIAL = [\n  { id: 'a1', title: 'Arrival', watched: true },\n  { id: 'p2', title: 'Past Lives', watched: false },\n  { id: 'd3', title: 'Dune: Part Two', watched: false },\n];\n\nfunction Watchlist() {\n  const [movies, setMovies] = useState(INITIAL);\n  const [left, setLeft] = useState(0);\n\n  // 1. This Effect only copies derived data into state. Replace it.\n  useEffect(() => {\n    setLeft(movies.filter((m) => !m.watched).length);\n  }, [movies]);\n\n  // 2. Sync document.title, and restore the old title on cleanup.\n\n  function toggle(id) {\n    setMovies(movies.map((m) => (m.id === id ? { ...m, watched: !m.watched } : m)));\n  }\n\n  return (\n    <section>\n      <p id=\"left\">{left} to watch</p>\n      <ul>\n        {movies.map((m) => (\n          <li key={m.id}>\n            {m.title}\n            <button onClick={() => toggle(m.id)}>{m.watched ? 'Watched' : 'Mark watched'}</button>\n          </li>\n        ))}\n      </ul>\n    </section>\n  );\n}\n\nexport default function App() {\n  const [show, setShow] = useState(true);\n  return (\n    <main>\n      <button id=\"hide\" onClick={() => setShow(false)}>Hide watchlist</button>\n      {show && <Watchlist />}\n    </main>\n  );\n}\n"},"solution":{"jsx":"import { useEffect, useState } from 'react';\n\n// The page's own title, before the Watchlist changes it.\ndocument.title = 'Playground';\n\nconst INITIAL = [\n  { id: 'a1', title: 'Arrival', watched: true },\n  { id: 'p2', title: 'Past Lives', watched: false },\n  { id: 'd3', title: 'Dune: Part Two', watched: false },\n];\n\nfunction Watchlist() {\n  const [movies, setMovies] = useState(INITIAL);\n  const left = movies.filter((m) => !m.watched).length;\n\n  useEffect(() => {\n    const previous = document.title;\n    document.title = `${left} to watch · Watchlist`;\n    return () => {\n      document.title = previous;\n    };\n  }, [left]);\n\n  function toggle(id) {\n    setMovies(movies.map((m) => (m.id === id ? { ...m, watched: !m.watched } : m)));\n  }\n\n  return (\n    <section>\n      <p id=\"left\">{left} to watch</p>\n      <ul>\n        {movies.map((m) => (\n          <li key={m.id}>\n            {m.title}\n            <button onClick={() => toggle(m.id)}>{m.watched ? 'Watched' : 'Mark watched'}</button>\n          </li>\n        ))}\n      </ul>\n    </section>\n  );\n}\n\nexport default function App() {\n  const [show, setShow] = useState(true);\n  return (\n    <main>\n      <button id=\"hide\" onClick={() => setShow(false)}>Hide watchlist</button>\n      {show && <Watchlist />}\n    </main>\n  );\n}\n"},"checks":[{"id":"count-derived","test":"return text('#left') === '2 to watch';"},{"id":"title-initial","test":"return document.title === '2 to watch · Watchlist';"},{"id":"title-updates","test":"const toggle = (t) => $$('li').find((li) => text(li).startsWith(t)).querySelector('button'); await click(toggle('Past Lives')); return document.title === '1 to watch · Watchlist' && text('#left') === '1 to watch';"},{"id":"title-cleanup","test":"await click('#hide'); return !$('#left') && document.title === 'Playground';"}],"title":"Sync the tab title","prompt":"<p>Two jobs in <code dir=\"ltr\">Watchlist</code>:</p>\n<ol>\n<li><strong>Remove the unnecessary Effect.</strong> <code dir=\"ltr\">left</code> is copied into state by an Effect. Calculate it during render instead.</li>\n<li><strong>Sync the browser tab title.</strong> While the Watchlist is on screen, <code dir=\"ltr\">document.title</code> should read <strong>2 to watch · Watchlist</strong> (with the real number), and update when a movie is marked watched. When the Watchlist is removed (click <strong>Hide watchlist</strong>), the title must go back to what it was before, <strong>Playground</strong>.</li>\n</ol>\n","hints":["Replace the <code dir=\"ltr\">left</code> state and its Effect with <code dir=\"ltr\">const left = movies.filter((m) =&gt; !m.watched).length;</code>","The title Effect reads <code dir=\"ltr\">left</code>, so its dependency array is <code dir=\"ltr\">[left]</code>.","Save the old title inside the Effect (<code dir=\"ltr\">const previous = document.title;</code>) and return a cleanup that puts it back."],"explanation":"<p><code dir=\"ltr\">left</code> is derived from <code dir=\"ltr\">movies</code>, so it’s calculated during render. The Effect version rendered once with a wrong count of 0 and then again with the right one. The document title, on the other hand, lives outside React, so syncing it is a real Effect. Listing <code dir=\"ltr\">[left]</code> re-runs it only when the count changes, and the cleanup restores the previous title both before each re-run and when the component unmounts. That’s also exactly what StrictMode’s mount, cleanup, mount check exercises in development.</p>\n","checkLabels":{"count-derived":"The page shows “2 to watch”","title-initial":"The tab title is “2 to watch · Watchlist”","title-updates":"Marking Past Lives watched updates the title to “1 to watch · Watchlist”","title-cleanup":"Hiding the Watchlist restores the title “Playground”"}},"words":810,"readMinutes":4}