[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-react-weekly-derived-state-bug":44,"search-suggestions":60,"quiz-article-react-weekly-derived-state-bug":107},[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,{"id":45,"slug":46,"kind":47,"title":48,"description":49,"config":50,"verticalId":5,"vertical":55,"course":52,"_count":56,"access":57,"attempts":59,"questionCount":51},"01a0ab57-ddfa-7328-8b07-065eb9829fae","react-weekly-derived-state-bug","PRACTICE_QUIZ","React Derived State","Test what you just learned about spotting derived state, why syncing it with useEffect costs an extra render, and when useMemo or a key actually applies.",{"questionCount":51,"timeLimitSec":52,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":53,"maxAttempts":52,"revealAnswers":54,"allowFlagging":18,"allowBacktracking":17},7,null,70,"AFTER_SUBMIT",{"slug":6,"name":7},{"questions":51},{"allowed":17,"reason":58},"FREE",[],[61,65,69,73,77,81,85,89,93,97,100,104],{"slug":62,"name":63,"articles":64},"webdev","Webdev",103,{"slug":66,"name":67,"articles":68},"javascript","Javascript",87,{"slug":70,"name":71,"articles":72},"frontend","Frontend",71,{"slug":74,"name":75,"articles":76},"css","Css",34,{"slug":78,"name":79,"articles":80},"tutorial","Tutorial",32,{"slug":82,"name":83,"articles":84},"typescript","Typescript",15,{"slug":86,"name":87,"articles":88},"react","React",13,{"slug":90,"name":91,"articles":92},"performance","Performance",12,{"slug":94,"name":95,"articles":96},"browser","Browser",9,{"slug":98,"name":99,"articles":96},"node","Node",{"slug":101,"name":102,"articles":103},"html","Html",6,{"slug":105,"name":106,"articles":103},"grammar","Grammar",{"id":108,"slug":46,"title":109,"subtitle":52,"excerpt":110,"coverUrl":111,"locale":13,"readingMinutes":112,"publishedAt":113,"viewCount":114,"likeCount":19,"commentCount":19,"author":115,"vertical":120,"topic":121,"tags":122,"_count":127,"playground":129,"body":131,"bodyMd":467,"seo":468,"translationGroupId":470,"series":471,"podcastUrl":52,"verticalId":5,"thread":488,"assessments":490,"translations":493,"quiz":495},"01a0ab57-dda2-74dd-816b-a0ef7f77aeb3","React Derived State: Why That useState Is Probably a Bug","React derived state explained: why syncing a computed value into useState with an Effect causes extra renders and drift, and what to write instead.","\u002Fmedia\u002Fcovers\u002Freact-weekly-derived-state-bug.png",14,"2026-09-20T16:27:07.271Z",61,{"id":116,"name":117,"username":118,"avatarUrl":52,"headline":119},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":86,"name":87},[123,124,125,126],{"slug":86,"name":87,"color":52},{"slug":66,"name":67,"color":52},{"slug":78,"name":79,"color":52},{"slug":62,"name":63,"color":52},{"assessments":128},1,{"slug":46,"title":130},"React derived state — interactive playground",{"blocks":132,"version":128},[133,137,140,143,146,151,154,163,166,169,172,186,189,192,198,201,208,211,214,217,220,225,228,231,235,238,242,245,248,251,255,258,261,264,267,271,274,277,280,283,287,290,294,297,300,303,306,309,312,315,323,326,329,336,339,342,346,349,353,356,359,362,365,368,372,375,378,381,384,387,390,393,425,429,432,440,443,446,449,452,455,458,461],{"id":134,"html":135,"type":136},"b1","\u003Cp>You add a search box to a todo list. \u003Ccode>todos\u003C\u002Fcode> is state, \u003Ccode>filter\u003C\u002Fcode> is state, and — because the filtered list &quot;depends on&quot; both — you add a third state variable, \u003Ccode>visibleTodos\u003C\u002Fcode>, and a \u003Ccode>useEffect\u003C\u002Fcode> that recalculates it whenever \u003Ccode>todos\u003C\u002Fcode> or \u003Ccode>filter\u003C\u002Fcode> changes. It works. Then, months later, someone adds a bulk &quot;complete all&quot; button that updates \u003Ccode>todos\u003C\u002Fcode> directly, and the list on screen doesn&#39;t change for a beat. No error, no warning — just a stale list until the next keystroke nudges the Effect awake.\u003C\u002Fp>","paragraph",{"id":138,"html":139,"type":136},"b2","\u003Cp>Nothing here is exotic. It&#39;s one of the most common bugs in React codebases, and it exists because a value that should have been \u003Cem>computed\u003C\u002Fem> got stored instead.\u003C\u002Fp>",{"id":141,"html":142,"type":136},"b3","\u003Cp>This is episode two of \u003Cstrong>React Deep Dive\u003C\u002Fstrong>, on what React itself decides rather than JavaScript wearing a React import. This one is about a decision every component makes constantly and mostly gets right by accident: which values belong in state, and which ones only look like they do.\u003C\u002Fp>",{"id":144,"html":145,"type":136},"b4","\u003Cp>This article is written against \u003Cstrong>React 19.3\u003C\u002Fstrong> (verified against the React blog and GitHub releases, 19.3.0, published September 9, 2026) and the \u003Cstrong>React Compiler at 1.0\u003C\u002Fstrong>, stable since October 2025. Everything here assumes React 19-era function components and hooks.\u003C\u002Fp>",{"id":147,"html":148,"text":149,"type":150,"level":31},"b5","What you&#39;ll learn","What you'll learn","heading",{"id":152,"html":153,"type":136},"b6","\u003Cp>By the end of this article you&#39;ll be able to:\u003C\u002Fp>",{"id":155,"type":156,"items":157,"ordered":18},"b7","list",[158,159,160,161,162],"Recognize derived state — a value fully determined by props or other state — versus state that genuinely needs to exist","Explain, precisely, why syncing a derived value with \u003Ccode>useEffect\u003C\u002Fcode> + \u003Ccode>setState\u003C\u002Fcode> costs an extra render and can drift","Replace that pattern with a plain calculation during render, and know when to reach for \u003Ccode>useMemo\u003C\u002Fcode> instead","Handle the harder case — resetting or adjusting state when a prop changes — without an Effect","Tell the difference between &quot;derived from what I already have&quot; and &quot;genuinely new information,&quot; which is the actual boundary",{"id":164,"html":165,"text":165,"type":150,"level":31},"b8","Who this is for",{"id":167,"html":168,"type":136},"b9","\u003Cp>You&#39;ve written function components with \u003Ccode>useState\u003C\u002Fcode> and \u003Ccode>useEffect\u003C\u002Fcode>, and you&#39;ve shipped at least one bug where two pieces of state disagreed with each other. No prior knowledge of the React Compiler is assumed.\u003C\u002Fp>",{"id":170,"html":171,"text":171,"type":150,"level":31},"b10","Table of contents",{"id":173,"type":156,"items":174,"ordered":18},"b11",[175,176,177,178,179,180,181,182,183,184,185],"\u003Ca href=\"#the-problem-a-list-that-lags-behind-its-own-data\">The problem: a list that lags behind its own data\u003C\u002Fa>","\u003Ca href=\"#the-mental-model-state-is-memory-render-is-a-formula\">The mental model: state is memory, render is a formula\u003C\u002Fa>","\u003Ca href=\"#stage-1-the-naive-fix-and-why-it-still-isnt-right\">Stage 1: the naive fix and why it still isn&#39;t right\u003C\u002Fa>","\u003Ca href=\"#stage-2-delete-the-state-keep-the-value\">Stage 2: delete the state, keep the value\u003C\u002Fa>","\u003Ca href=\"#stage-3-when-the-calculation-is-actually-expensive\">Stage 3: when the calculation is actually expensive\u003C\u002Fa>","\u003Ca href=\"#stage-4-the-harder-case-resetting-state-when-a-prop-changes\">Stage 4: the harder case — resetting state when a prop changes\u003C\u002Fa>","\u003Ca href=\"#edge-cases-and-gotchas\">Edge cases and gotchas\u003C\u002Fa>","\u003Ca href=\"#best-practices-the-actual-test\">Best practices: the actual test\u003C\u002Fa>","\u003Ca href=\"#faq\">FAQ\u003C\u002Fa>","\u003Ca href=\"#cheat-sheet\">Cheat sheet\u003C\u002Fa>","\u003Ca href=\"#key-takeaways\">Key takeaways\u003C\u002Fa>",{"id":187,"html":188,"text":188,"type":150,"level":31},"b12","The problem: a list that lags behind its own data",{"id":190,"html":191,"type":136},"b13","\u003Cp>Here&#39;s the todo list from the opening, written the way it tends to get written the first time:\u003C\u002Fp>",{"id":193,"code":194,"type":195,"language":196,"highlight":197},"b14","function TodoList({ todos }) {\n  const [filter, setFilter] = useState(\"all\");\n  const [visibleTodos, setVisibleTodos] = useState(todos);\n\n  useEffect(() => {\n    setVisibleTodos(\n      filter === \"all\" ? todos : todos.filter((t) => t.done === (filter === \"done\"))\n    );\n  }, [todos, filter]);\n\n  return (\n    \u003C>\n      \u003CFilterButtons value={filter} onChange={setFilter} \u002F>\n      \u003Cul>\n        {visibleTodos.map((t) => \u003Cli key={t.id}>{t.text}\u003C\u002Fli>)}\n      \u003C\u002Ful>\n    \u003C\u002F>\n  );\n}","code","jsx",[],{"id":199,"html":200,"type":136},"b15","\u003Cp>This runs. It even looks reasonable — \u003Ccode>visibleTodos\u003C\u002Fcode> &quot;depends on&quot; \u003Ccode>todos\u003C\u002Fcode> and \u003Ccode>filter\u003C\u002Fcode>, so it lives in an Effect that watches both. But walk through what actually happens on a single click of a filter button:\u003C\u002Fp>",{"id":202,"type":156,"items":203,"ordered":17},"b16",[204,205,206,207],"\u003Ccode>setFilter(&quot;done&quot;)\u003C\u002Fcode> schedules a render. \u003Ccode>TodoList\u003C\u002Fcode> re-renders with the \u003Cem>new\u003C\u002Fem> \u003Ccode>filter\u003C\u002Fcode> but the \u003Cem>old\u003C\u002Fem> \u003Ccode>visibleTodos\u003C\u002Fcode> — React hasn&#39;t run your Effect yet, because Effects run \u003Cstrong>after\u003C\u002Fstrong> the DOM commits.","The user briefly sees the wrong list (all todos, not just the done ones), for one paint.","The Effect then runs, calls \u003Ccode>setVisibleTodos\u003C\u002Fcode>, and schedules a \u003Cstrong>second\u003C\u002Fstrong> render.","React renders again, this time with the correct filtered list.",{"id":209,"html":210,"type":136},"b17","\u003Cp>That&#39;s two full render passes and one commit doing visible work for a value you could have had correct on the first pass. And the bulk &quot;complete all&quot; bug from the intro is the same mechanism from the other direction: something mutates \u003Ccode>todos\u003C\u002Fcode> through a path that doesn&#39;t also re-run this specific Effect&#39;s mental model correctly, or a later render reads \u003Ccode>visibleTodos\u003C\u002Fcode> before the Effect catches up, and the two state variables disagree.\u003C\u002Fp>",{"id":212,"html":213,"type":136},"b18","\u003Cp>None of this is a React bug. React is doing exactly what you asked: keep two separate pieces of memory, and use an Effect to keep the second one following the first. The bug is that \u003Ccode>visibleTodos\u003C\u002Fcode> was never independent information — it was a formula wearing state&#39;s clothes.\u003C\u002Fp>",{"id":215,"html":216,"text":216,"type":150,"level":31},"b19","The mental model: state is memory, render is a formula",{"id":218,"html":219,"type":136},"b20","\u003Cp>Split every value your component touches into two categories:\u003C\u002Fp>",{"id":221,"type":156,"items":222,"ordered":18},"b21",[223,224],"\u003Cstrong>State\u003C\u002Fstrong> is memory. It&#39;s the only thing React can&#39;t reconstruct on its own — user input, a value from a request, anything genuinely new that arrived from outside this render.","\u003Cstrong>A derived value\u003C\u002Fstrong> is anything you could recompute, right now, from state and props you already have. It isn&#39;t information; it&#39;s a formula over information.",{"id":226,"html":227,"type":136},"b22","\u003Cp>\u003Cstrong>The mental model:\u003C\u002Fstrong> if you can write \u003Ccode>const x = f(props, otherState)\u003C\u002Fcode> and get the right answer every time, \u003Ccode>x\u003C\u002Fcode> was never state — it&#39;s a calculation, and calculations belong in the render body, not in a \u003Ccode>useState\u003C\u002Fcode>\u002F\u003Ccode>useEffect\u003C\u002Fcode> pair. An Effect exists to synchronize your component with something \u003Cstrong>outside React\u003C\u002Fstrong> — the DOM, a subscription, a network request, \u003Ccode>document.title\u003C\u002Fcode>. Using one to copy one piece of React state into another piece of React state is React talking to itself through a detour, and the detour is where the extra render and the drift both come from.\u003C\u002Fp>",{"id":229,"html":230,"type":136},"b23","\u003Cp>This reframes \u003Ccode>useEffect\u003C\u002Fcode> itself: it isn&#39;t &quot;the place derived stuff goes,&quot; it&#39;s &quot;the place \u003Cem>synchronization with the outside world\u003C\u002Fem> goes.&quot; A value computed from props and state was never outside anything.\u003C\u002Fp>",{"id":232,"html":233,"text":234,"type":150,"level":31},"b24","Stage 1: the naive fix and why it still isn&#39;t right","Stage 1: the naive fix and why it still isn't right",{"id":236,"html":237,"type":136},"b25","\u003Cp>A common first correction is to memoize inside the Effect, or to add a guard so it &quot;only runs when needed&quot;:\u003C\u002Fp>",{"id":239,"code":240,"type":195,"language":196,"highlight":241},"b26","useEffect(() => {\n  const next = filter === \"all\" ? todos : todos.filter((t) => t.done === (filter === \"done\"));\n  if (next !== visibleTodos) setVisibleTodos(next);\n}, [todos, filter]);",[],{"id":243,"html":244,"type":136},"b27","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> this treats the symptom (an extra render) without touching the cause (a second copy of information that has to be kept honest). The stale-paint flash from step 2 above is still there — the Effect still runs after the commit, not before it — and you&#39;ve added a comparison that has to be maintained forever. The state was the mistake; no amount of guarding the Effect fixes that.\u003C\u002Fp>",{"id":246,"html":247,"text":247,"type":150,"level":31},"b28","Stage 2: delete the state, keep the value",{"id":249,"html":250,"type":136},"b29","\u003Cp>The actual fix removes code:\u003C\u002Fp>",{"id":252,"code":253,"type":195,"language":196,"highlight":254},"b30","function TodoList({ todos }) {\n  const [filter, setFilter] = useState(\"all\");\n\n  const visibleTodos =\n    filter === \"all\" ? todos : todos.filter((t) => t.done === (filter === \"done\"));\n\n  return (\n    \u003C>\n      \u003CFilterButtons value={filter} onChange={setFilter} \u002F>\n      \u003Cul>\n        {visibleTodos.map((t) => \u003Cli key={t.id}>{t.text}\u003C\u002Fli>)}\n      \u003C\u002Ful>\n    \u003C\u002F>\n  );\n}",[],{"id":256,"html":257,"type":136},"b31","\u003Cp>No \u003Ccode>visibleTodos\u003C\u002Fcode> state, no Effect, no second render, no drift — because there is only one piece of information (\u003Ccode>todos\u003C\u002Fcode> and \u003Ccode>filter\u003C\u002Fcode>) and one formula over it. Click a filter button now, and \u003Ccode>TodoList\u003C\u002Fcode> renders exactly once, with the correct list, because the correct list was never anything but \u003Ccode>todos\u003C\u002Fcode> and \u003Ccode>filter\u003C\u002Fcode> combined.\u003C\u002Fp>",{"id":259,"html":260,"type":136},"b32","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> a value that&#39;s recomputed on every render is not &quot;wasted work&quot; by default — rendering is supposed to be cheap and pure. Reach for a second render only when React genuinely needs one; a plain \u003Ccode>const\u003C\u002Fcode> inside the component body isn&#39;t a render, it&#39;s a step within the one you&#39;re already doing.\u003C\u002Fp>",{"id":262,"html":263,"text":263,"type":150,"level":31},"b33","Stage 3: when the calculation is actually expensive",{"id":265,"html":266,"type":136},"b34","\u003Cp>Sometimes the formula really is costly — sorting or filtering thousands of rows, for instance — and recomputing it on every render (including ones triggered by something unrelated, like typing in an unrelated text field on the same component) is wasteful. That&#39;s what \u003Ccode>useMemo\u003C\u002Fcode> is for:\u003C\u002Fp>",{"id":268,"code":269,"type":195,"language":196,"highlight":270},"b35","const visibleTodos = useMemo(\n  () => (filter === \"all\" ? todos : todos.filter((t) => t.done === (filter === \"done\"))),\n  [todos, filter]\n);",[],{"id":272,"html":273,"type":136},"b36","\u003Cp>\u003Ccode>useMemo\u003C\u002Fcode> still computes the value \u003Cstrong>during render\u003C\u002Fstrong>, synchronously, before anything paints — it just skips redoing the work if \u003Ccode>todos\u003C\u002Fcode> and \u003Ccode>filter\u003C\u002Fcode> are unchanged since last time. It is a performance optimization, not a place to put a second copy of the answer, and it produces no second commit the way the Effect version did.\u003C\u002Fp>",{"id":275,"html":276,"type":136},"b37","\u003Cp>Episode one of this series (\u003Ca href=\"https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Freact-compiler-10-what-usememo-you-can-delete-hgm\">React Compiler 1.0: What \u003Ccode>useMemo\u003C\u002Fcode> You Can Delete\u003C\u002Fa>) covers the other side of this: with the Compiler enabled, this specific \u003Ccode>useMemo\u003C\u002Fcode> is usually one you no longer have to write by hand — the compiler memoizes it for you. What it will never do is turn a \u003Ccode>useEffect\u003C\u002Fcode>-plus-\u003Ccode>setState\u003C\u002Fcode> pair back into a derived value; that rewrite is a design decision, not something a compiler can safely infer, because it can&#39;t know your Effect wasn&#39;t also doing something with genuine side effects on the outside world.\u003C\u002Fp>",{"id":278,"html":279,"text":279,"type":150,"level":31},"b38","Stage 4: the harder case — resetting state when a prop changes",{"id":281,"html":282,"type":136},"b39","\u003Cp>Sometimes what looks like &quot;syncing derived state&quot; is really about a prop change that should reset unrelated state — a \u003Ccode>&lt;ProfilePanel userId&gt;\u003C\u002Fcode> where switching users should clear a draft comment, for instance. The Effect-shaped instinct is the same trap:\u003C\u002Fp>",{"id":284,"code":285,"type":195,"language":196,"highlight":286},"b40","\u002F\u002F Don't do this\nfunction ProfilePanel({ userId }) {\n  const [comment, setComment] = useState(\"\");\n  useEffect(() => {\n    setComment(\"\");\n  }, [userId]);\n  \u002F\u002F ...\n}",[],{"id":288,"html":289,"type":136},"b41","\u003Cp>This has the identical problem as Stage 1: one render with the stale comment, then an Effect-triggered second render that clears it. The fix here isn&#39;t a calculation — \u003Ccode>comment\u003C\u002Fcode> genuinely is state, freely typed by the user, not derivable from anything — the fix is telling React this is a \u003Cstrong>different instance\u003C\u002Fstrong> of the component, which is a subject the first episode of this series covered in depth: giving the component a \u003Ccode>key\u003C\u002Fcode>.\u003C\u002Fp>",{"id":291,"code":292,"type":195,"language":196,"highlight":293},"b42","\u003CProfilePanel userId={userId} key={userId} \u002F>",[],{"id":295,"html":296,"type":136},"b43","\u003Cp>When \u003Ccode>userId\u003C\u002Fcode> changes, React doesn&#39;t update the existing \u003Ccode>ProfilePanel\u003C\u002Fcode> instance — it discards it and mounts a fresh one, with \u003Ccode>comment\u003C\u002Fcode> back at its initial value, in the \u003Cem>same\u003C\u002Fem> render, no Effect involved. See \u003Ca href=\"https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Freact-re-render-vs-remount-what-actually-triggers-each-5fok\">React Re-render vs Remount: What Actually Triggers Each\u003C\u002Fa> for exactly how React decides between updating an instance and replacing it. The two bugs in this article and that one are the same shape from two directions: this one is about values you shouldn&#39;t have kept as separate memory at all; that one is about memory you correctly kept, but attached to the wrong lifetime.\u003C\u002Fp>",{"id":298,"html":299,"type":136},"b44","\u003C!-- playground:start -->",{"id":301,"html":302,"text":302,"type":150,"level":31},"b45","🎮 Try it yourself",{"id":304,"html":305,"type":136},"b46","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Freact-weekly-derived-state-bug\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":307,"html":308,"type":136},"b47","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":310,"html":311,"type":136},"b48","\u003C!-- playground:end -->",{"id":313,"html":314,"text":314,"type":150,"level":31},"b49","Edge cases and gotchas",{"id":316,"type":156,"items":317,"ordered":18},"b50",[318,319,320,321,322],"\u003Cstrong>Adjusting only \u003Cem>some\u003C\u002Fem> state when a prop changes.\u003C\u002Fstrong> If a \u003Ccode>key\u003C\u002Fcode> reset would throw away too much (say, only one field should clear, the rest should survive), React&#39;s own guidance is to compute that one field during render by comparing the current prop against a \u003Ccode>previous\u003C\u002Fcode>-value stored in state — a narrow, deliberate escape hatch, not a general pattern, and one that reads oddly enough that it deserves a comment explaining why it isn&#39;t an Effect.","\u003Cstrong>Fetched data is not derived state.\u003C\u002Fstrong> \u003Ccode>todos\u003C\u002Fcode> arriving from an API is genuinely new information your component couldn&#39;t calculate on its own — that&#39;s real state (or better, a request-lifecycle tool that isn&#39;t plain \u003Ccode>useState\u003C\u002Fcode>). The rule in this article is about values computed \u003Cem>from\u003C\u002Fem> data you already hold, not the data itself.","\u003Cstrong>Derived values that also need to survive a remount.\u003C\u002Fstrong> If a formula&#39;s result should persist across a \u003Ccode>key\u003C\u002Fcode> change on purpose, it can&#39;t be &quot;just a calculation&quot; anymore — you&#39;ve described state, and that&#39;s fine; just be honest that it is one.","\u003Cstrong>\u003Ccode>useMemo\u003C\u002Fcode> is not a correctness tool.\u003C\u002Fstrong> It changes \u003Cem>when\u003C\u002Fem> the calculation reruns for performance, never \u003Cem>what\u003C\u002Fem> the calculation returns. Never rely on \u003Ccode>useMemo\u003C\u002Fcode> to skip work that has an externally visible side effect — that&#39;s what \u003Ccode>useEffect\u003C\u002Fcode> is for, and the two aren&#39;t interchangeable.","\u003Cstrong>Context values built from derived state.\u003C\u002Fstrong> A \u003Ccode>useMemo\u003C\u002Fcode>-wrapped object passed through \u003Ccode>Context.Provider\u003C\u002Fcode> is a very common and correct use of memoization — it stops every consumer from re-rendering just because the provider re-rendered with a fresh object literal. That&#39;s a real performance concern this pattern solves; it&#39;s a different subject from this article&#39;s bug, not evidence that all derived values need memoizing.",{"id":324,"html":325,"text":325,"type":150,"level":31},"b51","Best practices: the actual test",{"id":327,"html":328,"type":136},"b52","\u003Cp>Ask one question, in this order, every time a new value shows up in a component: \u003Cstrong>could I compute this, right now, from props and state I already have?\u003C\u002Fstrong>\u003C\u002Fp>",{"id":330,"type":156,"items":331,"ordered":18},"b53",[332,333,334,335],"\u003Cstrong>Yes, and it&#39;s cheap\u003C\u002Fstrong> → a plain \u003Ccode>const\u003C\u002Fcode> in the render body. No hook.","\u003Cstrong>Yes, but it&#39;s measurably expensive\u003C\u002Fstrong> → \u003Ccode>useMemo\u003C\u002Fcode>, still computed during render, still zero extra commits.","\u003Cstrong>No, this value is genuinely new information from outside React\u003C\u002Fstrong> (user input, a fetch response, a timer tick) → it&#39;s real state, and \u003Ccode>useEffect\u003C\u002Fcode> is the right tool if it also needs to synchronize with something outside React.","\u003Cstrong>A prop change should reset an entire component&#39;s state\u003C\u002Fstrong> → a \u003Ccode>key\u003C\u002Fcode>, not an Effect that calls several \u003Ccode>setState\u003C\u002Fcode>s in a row.",{"id":337,"html":338,"type":136},"b54","\u003Cp>If you ever catch yourself writing a \u003Ccode>useEffect\u003C\u002Fcode> whose only job is calling \u003Ccode>setState\u003C\u002Fcode> with a value derived purely from props and other state already in scope, that Effect is the bug, not the fix.\u003C\u002Fp>",{"id":340,"html":341,"text":341,"type":150,"level":31},"b55","FAQ",{"id":343,"html":344,"text":345,"type":150,"level":43},"b56","Is every \u003Ccode>useState\u003C\u002Fcode> that depends on props automatically derived state?","Is every useState that depends on props automatically derived state?",{"id":347,"html":348,"type":136},"b57","\u003Cp>Not automatically — the test is whether the \u003Cem>value\u003C\u002Fem> is fully determined by props and other state, not whether it merely reads them. A text input&#39;s \u003Ccode>value\u003C\u002Fcode> reads a \u003Ccode>defaultValue\u003C\u002Fcode> prop once, then holds independent user edits; that&#39;s real state that happened to be seeded from a prop, not a formula recomputed from it every render.\u003C\u002Fp>",{"id":350,"html":351,"text":352,"type":150,"level":43},"b58","Does using \u003Ccode>useMemo\u003C\u002Fcode> mean a value isn&#39;t derived state anymore?","Does using useMemo mean a value isn't derived state anymore?",{"id":354,"html":355,"type":136},"b59","\u003Cp>No — \u003Ccode>useMemo\u003C\u002Fcode> is still deriving the value during render from the same inputs; it only caches the result between renders with unchanged inputs. A \u003Ccode>useMemo\u003C\u002Fcode>&#39;d value and a plain \u003Ccode>const\u003C\u002Fcode> are the same \u003Cem>kind\u003C\u002Fem> of thing (a formula), differing only in whether the formula&#39;s cost justifies caching it.\u003C\u002Fp>",{"id":357,"html":358,"text":358,"type":150,"level":43},"b60","Why does the Effect-based version actually visibly flash the wrong content?",{"id":360,"html":361,"type":136},"b61","\u003Cp>Because Effects run after the browser has painted the render&#39;s output, not before it. The render with stale derived state is not skipped or invisible — it&#39;s a real commit the user can see, however briefly, before the Effect&#39;s \u003Ccode>setState\u003C\u002Fcode> triggers the corrected one.\u003C\u002Fp>",{"id":363,"html":364,"text":364,"type":150,"level":43},"b62","Does the React Compiler make this whole article unnecessary?",{"id":366,"html":367,"type":136},"b63","\u003Cp>No. The Compiler (stable at 1.0) automates \u003Cem>memoizing\u003C\u002Fem> values you already compute during render — it will not rewrite a \u003Ccode>useEffect\u003C\u002Fcode>-plus-\u003Ccode>setState\u003C\u002Fcode> pair into a derived calculation for you, because it can&#39;t safely know your Effect isn&#39;t also doing something with a real side effect. Removing the state is still a decision you make.\u003C\u002Fp>",{"id":369,"html":370,"text":371,"type":150,"level":43},"b64","What&#39;s the actual bug, in one sentence, if I never fix this?","What's the actual bug, in one sentence, if I never fix this?",{"id":373,"html":374,"type":136},"b65","\u003Cp>Two variables that are supposed to always agree occasionally won&#39;t, because one is memory and the other is a stale copy of a formula over that memory — and every real production codebase eventually hits the sequence of updates that makes them disagree.\u003C\u002Fp>",{"id":376,"html":377,"type":136},"b66","\u003C!-- quiz:start -->",{"id":379,"html":380,"text":380,"type":150,"level":31},"b67","🧠 Test yourself",{"id":382,"html":383,"type":136},"b68","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Freact-weekly-derived-state-bug\u002Fquiz\">Take the 7-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":385,"html":386,"type":136},"b69","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":388,"html":389,"type":136},"b70","\u003C!-- quiz:end -->",{"id":391,"html":392,"text":392,"type":150,"level":31},"b71","Cheat sheet",{"id":394,"head":395,"rows":399,"type":424},"b72",[396,397,398],"Situation","What it is","What to write",[400,404,408,412,416,420],[401,402,403],"Value fully computable from current props\u002Fstate","Derived value","\u003Ccode>const x = f(props, state)\u003C\u002Fcode> — no hook",[405,406,407],"Same, but the computation is measurably slow","Derived value, cached","\u003Ccode>useMemo(() =&gt; f(...), [deps])\u003C\u002Fcode>",[409,410,411],"Genuinely new info from outside React (input, fetch, timer)","State","\u003Ccode>useState\u003C\u002Fcode>, updated by the event\u002Fcallback that produced it",[413,414,415],"A prop change should reset a whole component&#39;s state","Identity change","\u003Ccode>&lt;Child key={propValue} \u002F&gt;\u003C\u002Fcode>",[417,418,419],"A prop change should reset only one field, rest must survive","Narrow adjustment","Compare a stored \u003Ccode>previous\u003C\u002Fcode> value during render — last resort, comment it",[421,422,423],"Object\u002Farray passed through Context that would otherwise churn","Derived value, cached for consumers","\u003Ccode>useMemo\u003C\u002Fcode> around the Provider&#39;s \u003Ccode>value\u003C\u002Fcode>","table",{"id":426,"code":427,"type":195,"language":196,"highlight":428},"b73","\u002F\u002F The one-line test, every time you reach for useState:\n\u002F\u002F \"Could I calculate this from props\u002Fstate I already have?\"\n\u002F\u002F Yes  -> const, or useMemo if it's expensive. No Effect.\n\u002F\u002F No   -> it's real state. useEffect only if it must sync with\n\u002F\u002F         something outside React (DOM, subscription, network).",[],{"id":430,"html":431,"text":431,"type":150,"level":31},"b74","Key takeaways",{"id":433,"type":156,"items":434,"ordered":18},"b75",[435,436,437,438,439],"If a value can be computed from props and state you already have, it isn&#39;t state — it&#39;s a formula, and it belongs in the render body.","A \u003Ccode>useEffect\u003C\u002Fcode> that calls \u003Ccode>setState\u003C\u002Fcode> with a derived value costs a real extra render (Effects run after the commit) and creates two copies of one fact, which can drift.","\u003Ccode>useMemo\u003C\u002Fcode> caches a derived calculation for performance; it never changes what the calculation returns and never replaces the decision to remove unnecessary state in the first place.","When a prop change should reset a component&#39;s state entirely, use \u003Ccode>key\u003C\u002Fcode>, not an Effect — it&#39;s a single render, not two.","The React Compiler automates memoizing derived values; it does not and cannot decide for you that a state variable shouldn&#39;t have existed.",{"id":441,"html":442,"text":442,"type":150,"level":31},"b76","Back to that todo list",{"id":444,"html":445,"type":136},"b77","\u003Cp>The fix for the opening bug was never a smarter Effect — it was noticing that \u003Ccode>visibleTodos\u003C\u002Fcode> had no information in it that \u003Ccode>todos\u003C\u002Fcode> and \u003Ccode>filter\u003C\u002Fcode> didn&#39;t already have. Delete the state, keep the formula, and the &quot;complete all&quot; bug and the one-paint flash both disappear, because there was never a second fact to get out of sync in the first place.\u003C\u002Fp>",{"id":447,"html":448,"type":136},"b78","\u003Cp>Next time you write \u003Ccode>useEffect(() =&gt; setSomething(...), [dep])\u003C\u002Fcode>, try the one-line test from this article before you commit it: could \u003Ccode>something\u003C\u002Fcode> just be a \u003Ccode>const\u003C\u002Fcode>? If yes, you&#39;ve probably found a bug before your users did.\u003C\u002Fp>",{"id":450,"html":451,"type":136},"b79","\u003Cp>What&#39;s the strangest derived-state bug you&#39;ve shipped — and how long did it take to notice? Drop it in the comments.\u003C\u002Fp>",{"id":453,"type":454},"b80","divider",{"id":456,"html":457,"type":136},"b81","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":459,"html":460,"type":136},"b82","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":462,"type":156,"items":463,"ordered":18},"b83",[464,465,466],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","You add a search box to a todo list. `todos` is state, `filter` is state, and — because the filtered list \"depends on\" both — you add a third state variable, `visibleTodos`, and a `useEffect` that recalculates it whenever `todos` or `filter` changes. It works. Then, months later, someone adds a bulk \"complete all\" button that updates `todos` directly, and the list on screen doesn't change for a beat. No error, no warning — just a stale list until the next keystroke nudges the Effect awake.\n\nNothing here is exotic. It's one of the most common bugs in React codebases, and it exists because a value that should have been *computed* got stored instead.\n\nThis is episode two of **React Deep Dive**, on what React itself decides rather than JavaScript wearing a React import. This one is about a decision every component makes constantly and mostly gets right by accident: which values belong in state, and which ones only look like they do.\n\nThis article is written against **React 19.3** (verified against the React blog and GitHub releases, 19.3.0, published September 9, 2026) and the **React Compiler at 1.0**, stable since October 2025. Everything here assumes React 19-era function components and hooks.\n\n## What you'll learn\n\nBy the end of this article you'll be able to:\n\n- Recognize derived state — a value fully determined by props or other state — versus state that genuinely needs to exist\n- Explain, precisely, why syncing a derived value with `useEffect` + `setState` costs an extra render and can drift\n- Replace that pattern with a plain calculation during render, and know when to reach for `useMemo` instead\n- Handle the harder case — resetting or adjusting state when a prop changes — without an Effect\n- Tell the difference between \"derived from what I already have\" and \"genuinely new information,\" which is the actual boundary\n\n## Who this is for\n\nYou've written function components with `useState` and `useEffect`, and you've shipped at least one bug where two pieces of state disagreed with each other. No prior knowledge of the React Compiler is assumed.\n\n## Table of contents\n\n- [The problem: a list that lags behind its own data](#the-problem-a-list-that-lags-behind-its-own-data)\n- [The mental model: state is memory, render is a formula](#the-mental-model-state-is-memory-render-is-a-formula)\n- [Stage 1: the naive fix and why it still isn't right](#stage-1-the-naive-fix-and-why-it-still-isnt-right)\n- [Stage 2: delete the state, keep the value](#stage-2-delete-the-state-keep-the-value)\n- [Stage 3: when the calculation is actually expensive](#stage-3-when-the-calculation-is-actually-expensive)\n- [Stage 4: the harder case — resetting state when a prop changes](#stage-4-the-harder-case-resetting-state-when-a-prop-changes)\n- [Edge cases and gotchas](#edge-cases-and-gotchas)\n- [Best practices: the actual test](#best-practices-the-actual-test)\n- [FAQ](#faq)\n- [Cheat sheet](#cheat-sheet)\n- [Key takeaways](#key-takeaways)\n\n## The problem: a list that lags behind its own data\n\nHere's the todo list from the opening, written the way it tends to get written the first time:\n\n```jsx\nfunction TodoList({ todos }) {\n  const [filter, setFilter] = useState(\"all\");\n  const [visibleTodos, setVisibleTodos] = useState(todos);\n\n  useEffect(() => {\n    setVisibleTodos(\n      filter === \"all\" ? todos : todos.filter((t) => t.done === (filter === \"done\"))\n    );\n  }, [todos, filter]);\n\n  return (\n    \u003C>\n      \u003CFilterButtons value={filter} onChange={setFilter} \u002F>\n      \u003Cul>\n        {visibleTodos.map((t) => \u003Cli key={t.id}>{t.text}\u003C\u002Fli>)}\n      \u003C\u002Ful>\n    \u003C\u002F>\n  );\n}\n```\n\nThis runs. It even looks reasonable — `visibleTodos` \"depends on\" `todos` and `filter`, so it lives in an Effect that watches both. But walk through what actually happens on a single click of a filter button:\n\n1. `setFilter(\"done\")` schedules a render. `TodoList` re-renders with the *new* `filter` but the *old* `visibleTodos` — React hasn't run your Effect yet, because Effects run **after** the DOM commits.\n2. The user briefly sees the wrong list (all todos, not just the done ones), for one paint.\n3. The Effect then runs, calls `setVisibleTodos`, and schedules a **second** render.\n4. React renders again, this time with the correct filtered list.\n\nThat's two full render passes and one commit doing visible work for a value you could have had correct on the first pass. And the bulk \"complete all\" bug from the intro is the same mechanism from the other direction: something mutates `todos` through a path that doesn't also re-run this specific Effect's mental model correctly, or a later render reads `visibleTodos` before the Effect catches up, and the two state variables disagree.\n\nNone of this is a React bug. React is doing exactly what you asked: keep two separate pieces of memory, and use an Effect to keep the second one following the first. The bug is that `visibleTodos` was never independent information — it was a formula wearing state's clothes.\n\n## The mental model: state is memory, render is a formula\n\nSplit every value your component touches into two categories:\n\n- **State** is memory. It's the only thing React can't reconstruct on its own — user input, a value from a request, anything genuinely new that arrived from outside this render.\n- **A derived value** is anything you could recompute, right now, from state and props you already have. It isn't information; it's a formula over information.\n\n**The mental model:** if you can write `const x = f(props, otherState)` and get the right answer every time, `x` was never state — it's a calculation, and calculations belong in the render body, not in a `useState`\u002F`useEffect` pair. An Effect exists to synchronize your component with something **outside React** — the DOM, a subscription, a network request, `document.title`. Using one to copy one piece of React state into another piece of React state is React talking to itself through a detour, and the detour is where the extra render and the drift both come from.\n\nThis reframes `useEffect` itself: it isn't \"the place derived stuff goes,\" it's \"the place *synchronization with the outside world* goes.\" A value computed from props and state was never outside anything.\n\n## Stage 1: the naive fix and why it still isn't right\n\nA common first correction is to memoize inside the Effect, or to add a guard so it \"only runs when needed\":\n\n```jsx\nuseEffect(() => {\n  const next = filter === \"all\" ? todos : todos.filter((t) => t.done === (filter === \"done\"));\n  if (next !== visibleTodos) setVisibleTodos(next);\n}, [todos, filter]);\n```\n\n**Key concept:** this treats the symptom (an extra render) without touching the cause (a second copy of information that has to be kept honest). The stale-paint flash from step 2 above is still there — the Effect still runs after the commit, not before it — and you've added a comparison that has to be maintained forever. The state was the mistake; no amount of guarding the Effect fixes that.\n\n## Stage 2: delete the state, keep the value\n\nThe actual fix removes code:\n\n```jsx\nfunction TodoList({ todos }) {\n  const [filter, setFilter] = useState(\"all\");\n\n  const visibleTodos =\n    filter === \"all\" ? todos : todos.filter((t) => t.done === (filter === \"done\"));\n\n  return (\n    \u003C>\n      \u003CFilterButtons value={filter} onChange={setFilter} \u002F>\n      \u003Cul>\n        {visibleTodos.map((t) => \u003Cli key={t.id}>{t.text}\u003C\u002Fli>)}\n      \u003C\u002Ful>\n    \u003C\u002F>\n  );\n}\n```\n\nNo `visibleTodos` state, no Effect, no second render, no drift — because there is only one piece of information (`todos` and `filter`) and one formula over it. Click a filter button now, and `TodoList` renders exactly once, with the correct list, because the correct list was never anything but `todos` and `filter` combined.\n\n**Key concept:** a value that's recomputed on every render is not \"wasted work\" by default — rendering is supposed to be cheap and pure. Reach for a second render only when React genuinely needs one; a plain `const` inside the component body isn't a render, it's a step within the one you're already doing.\n\n## Stage 3: when the calculation is actually expensive\n\nSometimes the formula really is costly — sorting or filtering thousands of rows, for instance — and recomputing it on every render (including ones triggered by something unrelated, like typing in an unrelated text field on the same component) is wasteful. That's what `useMemo` is for:\n\n```jsx\nconst visibleTodos = useMemo(\n  () => (filter === \"all\" ? todos : todos.filter((t) => t.done === (filter === \"done\"))),\n  [todos, filter]\n);\n```\n\n`useMemo` still computes the value **during render**, synchronously, before anything paints — it just skips redoing the work if `todos` and `filter` are unchanged since last time. It is a performance optimization, not a place to put a second copy of the answer, and it produces no second commit the way the Effect version did.\n\nEpisode one of this series (\u003Ca href=\"https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Freact-compiler-10-what-usememo-you-can-delete-hgm\">React Compiler 1.0: What `useMemo` You Can Delete\u003C\u002Fa>) covers the other side of this: with the Compiler enabled, this specific `useMemo` is usually one you no longer have to write by hand — the compiler memoizes it for you. What it will never do is turn a `useEffect`-plus-`setState` pair back into a derived value; that rewrite is a design decision, not something a compiler can safely infer, because it can't know your Effect wasn't also doing something with genuine side effects on the outside world.\n\n## Stage 4: the harder case — resetting state when a prop changes\n\nSometimes what looks like \"syncing derived state\" is really about a prop change that should reset unrelated state — a `\u003CProfilePanel userId>` where switching users should clear a draft comment, for instance. The Effect-shaped instinct is the same trap:\n\n```jsx\n\u002F\u002F Don't do this\nfunction ProfilePanel({ userId }) {\n  const [comment, setComment] = useState(\"\");\n  useEffect(() => {\n    setComment(\"\");\n  }, [userId]);\n  \u002F\u002F ...\n}\n```\n\nThis has the identical problem as Stage 1: one render with the stale comment, then an Effect-triggered second render that clears it. The fix here isn't a calculation — `comment` genuinely is state, freely typed by the user, not derivable from anything — the fix is telling React this is a **different instance** of the component, which is a subject the first episode of this series covered in depth: giving the component a `key`.\n\n```jsx\n\u003CProfilePanel userId={userId} key={userId} \u002F>\n```\n\nWhen `userId` changes, React doesn't update the existing `ProfilePanel` instance — it discards it and mounts a fresh one, with `comment` back at its initial value, in the *same* render, no Effect involved. See \u003Ca href=\"https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Freact-re-render-vs-remount-what-actually-triggers-each-5fok\">React Re-render vs Remount: What Actually Triggers Each\u003C\u002Fa> for exactly how React decides between updating an instance and replacing it. The two bugs in this article and that one are the same shape from two directions: this one is about values you shouldn't have kept as separate memory at all; that one is about memory you correctly kept, but attached to the wrong lifetime.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Freact-weekly-derived-state-bug\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## Edge cases and gotchas\n\n- **Adjusting only *some* state when a prop changes.** If a `key` reset would throw away too much (say, only one field should clear, the rest should survive), React's own guidance is to compute that one field during render by comparing the current prop against a `previous`-value stored in state — a narrow, deliberate escape hatch, not a general pattern, and one that reads oddly enough that it deserves a comment explaining why it isn't an Effect.\n- **Fetched data is not derived state.** `todos` arriving from an API is genuinely new information your component couldn't calculate on its own — that's real state (or better, a request-lifecycle tool that isn't plain `useState`). The rule in this article is about values computed *from* data you already hold, not the data itself.\n- **Derived values that also need to survive a remount.** If a formula's result should persist across a `key` change on purpose, it can't be \"just a calculation\" anymore — you've described state, and that's fine; just be honest that it is one.\n- **`useMemo` is not a correctness tool.** It changes *when* the calculation reruns for performance, never *what* the calculation returns. Never rely on `useMemo` to skip work that has an externally visible side effect — that's what `useEffect` is for, and the two aren't interchangeable.\n- **Context values built from derived state.** A `useMemo`-wrapped object passed through `Context.Provider` is a very common and correct use of memoization — it stops every consumer from re-rendering just because the provider re-rendered with a fresh object literal. That's a real performance concern this pattern solves; it's a different subject from this article's bug, not evidence that all derived values need memoizing.\n\n## Best practices: the actual test\n\nAsk one question, in this order, every time a new value shows up in a component: **could I compute this, right now, from props and state I already have?**\n\n- **Yes, and it's cheap** → a plain `const` in the render body. No hook.\n- **Yes, but it's measurably expensive** → `useMemo`, still computed during render, still zero extra commits.\n- **No, this value is genuinely new information from outside React** (user input, a fetch response, a timer tick) → it's real state, and `useEffect` is the right tool if it also needs to synchronize with something outside React.\n- **A prop change should reset an entire component's state** → a `key`, not an Effect that calls several `setState`s in a row.\n\nIf you ever catch yourself writing a `useEffect` whose only job is calling `setState` with a value derived purely from props and other state already in scope, that Effect is the bug, not the fix.\n\n## FAQ\n\n### Is every `useState` that depends on props automatically derived state?\n\nNot automatically — the test is whether the *value* is fully determined by props and other state, not whether it merely reads them. A text input's `value` reads a `defaultValue` prop once, then holds independent user edits; that's real state that happened to be seeded from a prop, not a formula recomputed from it every render.\n\n### Does using `useMemo` mean a value isn't derived state anymore?\n\nNo — `useMemo` is still deriving the value during render from the same inputs; it only caches the result between renders with unchanged inputs. A `useMemo`'d value and a plain `const` are the same *kind* of thing (a formula), differing only in whether the formula's cost justifies caching it.\n\n### Why does the Effect-based version actually visibly flash the wrong content?\n\nBecause Effects run after the browser has painted the render's output, not before it. The render with stale derived state is not skipped or invisible — it's a real commit the user can see, however briefly, before the Effect's `setState` triggers the corrected one.\n\n### Does the React Compiler make this whole article unnecessary?\n\nNo. The Compiler (stable at 1.0) automates *memoizing* values you already compute during render — it will not rewrite a `useEffect`-plus-`setState` pair into a derived calculation for you, because it can't safely know your Effect isn't also doing something with a real side effect. Removing the state is still a decision you make.\n\n### What's the actual bug, in one sentence, if I never fix this?\n\nTwo variables that are supposed to always agree occasionally won't, because one is memory and the other is a stale copy of a formula over that memory — and every real production codebase eventually hits the sequence of updates that makes them disagree.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 7-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Freact-weekly-derived-state-bug\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\n## Cheat sheet\n\n| Situation | What it is | What to write |\n| --- | --- | --- |\n| Value fully computable from current props\u002Fstate | Derived value | `const x = f(props, state)` — no hook |\n| Same, but the computation is measurably slow | Derived value, cached | `useMemo(() => f(...), [deps])` |\n| Genuinely new info from outside React (input, fetch, timer) | State | `useState`, updated by the event\u002Fcallback that produced it |\n| A prop change should reset a whole component's state | Identity change | `\u003CChild key={propValue} \u002F>` |\n| A prop change should reset only one field, rest must survive | Narrow adjustment | Compare a stored `previous` value during render — last resort, comment it |\n| Object\u002Farray passed through Context that would otherwise churn | Derived value, cached for consumers | `useMemo` around the Provider's `value` |\n\n```jsx\n\u002F\u002F The one-line test, every time you reach for useState:\n\u002F\u002F \"Could I calculate this from props\u002Fstate I already have?\"\n\u002F\u002F Yes  -> const, or useMemo if it's expensive. No Effect.\n\u002F\u002F No   -> it's real state. useEffect only if it must sync with\n\u002F\u002F         something outside React (DOM, subscription, network).\n```\n\n## Key takeaways\n\n- If a value can be computed from props and state you already have, it isn't state — it's a formula, and it belongs in the render body.\n- A `useEffect` that calls `setState` with a derived value costs a real extra render (Effects run after the commit) and creates two copies of one fact, which can drift.\n- `useMemo` caches a derived calculation for performance; it never changes what the calculation returns and never replaces the decision to remove unnecessary state in the first place.\n- When a prop change should reset a component's state entirely, use `key`, not an Effect — it's a single render, not two.\n- The React Compiler automates memoizing derived values; it does not and cannot decide for you that a state variable shouldn't have existed.\n\n## Back to that todo list\n\nThe fix for the opening bug was never a smarter Effect — it was noticing that `visibleTodos` had no information in it that `todos` and `filter` didn't already have. Delete the state, keep the formula, and the \"complete all\" bug and the one-paint flash both disappear, because there was never a second fact to get out of sync in the first place.\n\nNext time you write `useEffect(() => setSomething(...), [dep])`, try the one-line test from this article before you commit it: could `something` just be a `const`? If yes, you've probably found a bug before your users did.\n\nWhat's the strangest derived-state bug you've shipped — and how long did it take to notice? Drop it in the comments.\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":109,"canonical":469,"description":110},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Freact-weekly-derived-state-bug","01a0ab57-dda2-74dd-816b-a5f3b8e1d677",{"name":472,"part":473,"total":473,"items":474},"React Deep Dive",4,[475,479,483,487],{"slug":476,"title":477,"publishedAt":478,"readingMinutes":88},"react-weekly-rerender-vs-remount","React Re-render vs Remount: What Actually Triggers Each","2026-08-29T12:32:41.200Z",{"slug":480,"title":481,"publishedAt":482,"readingMinutes":88},"react-weekly-compiler-memoization","React Compiler 1.0: What useMemo You Can Delete","2026-09-05T10:17:49.849Z",{"slug":484,"title":485,"publishedAt":486,"readingMinutes":88},"react-weekly-form-actions-pending-state","React Form Actions: useActionState & useFormStatus Guide","2026-09-12T10:23:52.928Z",{"slug":46,"title":109,"publishedAt":113,"readingMinutes":112},{"id":489,"locked":18},"01a0ab57-ddde-7209-8ee9-6b88b01630b7",[491],{"id":45,"slug":46,"title":48,"_count":492},{"questions":51},[494],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":496,"questionCount":51},{"questions":51}]