## Search terms getDerivedStateFromProps, useState, useEffect, derived state ## Previous issues - #14288 - #14738 - #14830 - #15523 - #16461 - #17712 ## Essential reading - [1] [You Might Not Need an Effect > Adjusting some state when a prop changes](https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes) - [2] [You Probably Don't Need Derived State](https://legacy.reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html) (from the legacy docs) ## Abstract / TL;DR Adjusting state when a prop or context changes makes sense more often than the official docs would have you believe. The pattern suggested in [1] is extremely underrated, but also unnecessarily confusing. React should offer and promote a less obscure hook-based solution to the problem so as to avoid the suggested solution's complexity and prevent developers from misemploying `useEffect`. ## Motivation Here is the right way to reset state when some input changes, as suggested in [1]: ```typescript const [count, setCount] = useState(initialCount); const [prevInitialCount, setPrevInitialCount] = useState(initialCount); if (initialCount !== prevInitialCount) { setPrevInitialCount(initialCount); setCount(initialCount); } ``` The input in question (`initialCount` in this case) could be a prop, a context value, an input argument to a custom hook, or some value derived from a combination of those. From my experience, however, most people are either unaware of this pattern or find it too confusing to wrap their heads around, and therefore use an effect-based approach instead: ```typescript const [count, setCount] = useState(initialCount); useEffect(() => { setCount(initialCount); }, [initialCount]); ``` The prevalence of this approach is hard to overstate. Using an effect is suggested in top answers to pretty much all questions about syncing state to props on StackOverflow ([example](https://stackoverflow.com/questions/54865764/react-usestate-does-not-reload-state-from-props), [example](https://stackoverflow.com/questions/54625831/how-to-sync-props-to-state-using-react-hooks-setstate), [example](https://stackoverflow.com/questions/68730502/react-usestate-with-state-that-updates-based-on-props)). All AI assistants I've asked also suggested this as their recommended solution. But most importantly, this is the approach I see used in the wild all the time, even by devs much more experienced than I am. The problem with the approach is that effects only run after rendering is completed and, in most cases, after its results are committed to the DOM. This results in unnecessary rerenders and inconsistent intermediate states being displayed in the UI. Unnecessary rerenders are also a drawback of the approach suggested in [1] as I will show in a moment. Let's explore the drawbacks of both approaches with a more advanced example I've come up with. ```typescript import { createContext, useContext, useEffect, useMemo, useReducer, useState, } from "react"; import { createRoot } from "react-dom/client"; const ClientLanguageContext = createContext("en"); // English is the default function useClientLanguage() { return useContext(ClientLanguageContext); } function LanguageCheckboxes({ additionalLanguages, }: { additionalLanguages: string[]; }) { const clientLanguage = useClientLanguage(); const languages = useMemo( () => new Set([clientLanguage, ...additionalLanguages]), [clientLanguage, additionalLanguages] ); const [checkedLanguages, setCheckedLanguages] = useState(new Set<string>()); useEffect(() => { setCheckedLanguages((prev) => { // If the `languages` set has changed in a way that it no longer includes // some of the languages it included before, those removed languages also // have to be removed from `checkedLanguages`! // Compute the intersection of `languages` and previous `checkedLanguages`: const next = new Set( Array.from(prev).filter((language) => languages.has(language)) ); // Only actually adjust the value if languages have been removed: return next.size < prev.size ? next : prev; }); }, [languages]); return ( <> {Array.from(languages).map((language) => ( <div key={language}> <label> <input value={language} type="checkbox" checked={checkedLanguages.has(language)} onChange={(e) => { const nextCheckedLanguages = new Set(checkedLanguages); if (e.target.checked) nextCheckedLanguages.add(language); else nextCheckedLanguages.delete(language); setCheckedLanguages(nextCheckedLanguages); }} /> {language} </label> </div> ))} <p>Checked languages: {Array.from(checkedLanguages).join(", ")}</p> </> ); } const ADDITIONAL_LANGUAGES = ["de", "fr"]; function App() { const [clientLanguage, setClientLanguage] = useState("en"); const [additionalLanguages, toggleAdditionalLanguages] = useReducer( (prev) => (prev.length ? [] : ADDITIONAL_LANGUAGES), ADDITIONAL_LANGUAGES ); return ( <ClientLanguageContext value={clientLanguage}> <p> Client language: <br /> <input value={clientLanguage} onChange={(e) => setClientLanguage(e.target.value)} /> </p> <p> <button onClick={toggleAdditionalLanguages}> Toggle additional languages </button> </p> <LanguageCheckboxes additionalLanguages={additionalLanguages} /> </ClientLanguageContext> ); } createRoot(document.getElementById("root")!).render(<App />); ``` This example app does not offer any useful functionality, but it is good at demonstrating all sorts of issues related to derived state. As you can see, I've chosen the wrong `useEffect` approach for this initial implementation. If you check all available languages and then click the toggle button, you will notice that “de” and “fr” disappear faster from the checkboxes than from the “Checked languages” list on the bottom, where they stay a few milliseconds longer. The reason is that the effect adjusting `checkedLanguages` is only executed after the render caused by the `additionalLanguages` update is completed and its result is reflected in the UI. This is simply how effects work in React, and the fact that we get to see an invalid state in the UI because of that is what makes the `useEffect` approach to derived state an absolute no-go. Let's now try the approach suggested in [1]: ```typescript function LanguageCheckboxes({ additionalLanguages, }: { additionalLanguages: string[]; }) { const clientLanguage = useClientLanguage(); const languages = useMemo( () => new Set([clientLanguage, ...additionalLanguages]), [clientLanguage, additionalLanguages] ); const [checkedLanguages, setCheckedLanguages] = useState(new Set<string>()); const [prevLanguages, setPrevLanguages] = useState(languages); if (languages !== prevLanguages) { setPrevLanguages(languages); // If the `languages` set has changed in a way that it no longer includes // some of the languages it included before, those removed languages also // have to be removed from `checkedLanguages`! // Compute the intersection of `languages` and `checkedLanguages`: const nextCheckedLanguages = new Set( Array.from(checkedLanguages).filter((language) => languages.has(language)) ); // Only adjust `checkedLanguages` if languages have been removed: if (nextCheckedLanguages.size < checkedLanguages.size) { setCheckedLanguages(nextCheckedLanguages); } } return /* ... */; } ``` Well, this does seem to work, but unfortunately, it is not a good solution either. The reason is that the value of `languages` is computed with the `useMemo` hook, which, according to [the docs](https://react.dev/reference/react/useMemo), should only be used as a performance optimization. The code we write should work perfectly fine without it, but that is not the case here: since without `useMemo`, the identity of `languages` would change on every render, `setPrevLanguages(languages)` would end up being called in an infinite loop! To overcome this complication, we have to store previous values of both inputs the `languages` variable is derived from (i.e. `clientLanguage` and `additionalLanguages`) instead of its own previous values: ```typescript function LanguageCheckboxes({ additionalLanguages, }: { additionalLanguages: string[]; }) { const clientLanguage = useClientLanguage(); const languages = useMemo( () => new Set([clientLanguage, ...additionalLanguages]), [clientLanguage, additionalLanguages] ); const [checkedLanguages, setCheckedLanguages] = useState(new Set<string>()); const [prevClientLanguage, setPrevClientLanguage] = useState(clientLanguage); const [prevAdditionalLanguages, setPrevAdditionalLanguages] = useState(additionalLanguages); if ( clientLanguage !== prevClientLanguage || additionalLanguages !== prevAdditionalLanguages ) { setPrevClientLanguage(clientLanguage); setPrevAdditionalLanguages(additionalLanguages); // If the `languages` set has changed in a way that it no longer includes // some of the languages it included before, those removed languages also // have to be removed from `checkedLanguages`! // Compute the intersection of `languages` and `checkedLanguages`: const nextCheckedLanguages = new Set( Array.from(checkedLanguages).filter((language) => languages.has(language)) ); // Only adjust `checkedLanguages` if languages have been removed: if (nextCheckedLanguages.size < checkedLanguages.size) { setCheckedLanguages(nextCheckedLanguages); } } return /* ... */; } ``` This is the correct code that I think adheres to all React's official recommendations, but oh boy, is it cumbersome, fragile and confusing! Imagine deciding to extend `languages` by elements from yet another source beside `clientLanguage` and `additionalLanguages`. How easy is it to forget to add a `prevX` state variable for this new source? The linter is not there to remind us! Furthermore, we haven't got rid of the unnecessary rerendering. The inconsistent result of the first render doesn't end up in the UI anymore and instead gets immediately thrown away as explained in [1], but still, that unnecessary first render does take place! Can we really not do better than that? One idea that often comes to mind in the context of state synchronization is to use a different value for the `key` attribute whenever some input that the component's state is derived from changes. This is the recommended approach in [2], but unfortunately, it barely solves anything. There is a bunch of problems with the approach: - It is useless in the context of custom hooks since there are no components involved in their definitions, and so we cannot really supply the key anywhere. - Changing the key causes *all* of the component's internal state to be reset, which is rarely what we want. - Changing the key causes a new DOM subtree to be created for the component. Besides being quite inefficient, this can also cause unwanted side effects such as the focused input within the component not being focused anymore after the remount. - If the component's state is derived from more than one input values, the developer has to come up with a clever way to combine those values into a key. I hope this is enough to show how bad this key solution is in most cases. Let's now have a look at the alternative I suggest. ## Proposal: Extend `useState` by a dependency array The alternative isn't new, it has been suggested before a couple of times. I particularly like this definition from #14738: > It would be useful if we could declare dependencies for `useState`, in the same way that we can for `useMemo`, and have the state reset back to the initial state if they change: > > ```javascript > const [choice, setChoice] = useState(options[0], [options]); > ``` > > In order to allow preserving the current value if it's valid, React could supply `prevState` to the initial state factory function, if any exists, e.g. > > ```javascript > const [choice, setChoice] = useState(prevState => { > if (prevState && options.includes(prevState) { > return prevState; > else { > return options[0]; > } > }, [options]); > ``` This is a very natural solution requiring pretty much no mind shift at all since it simply brings `useState` in line with the other hooks accepting a dependency array – a concept well-known to all React developers. The example with `LanguageCheckboxes` has demonstrated that in certain scenarios, adjusting state based on both the new input and the previous state value is needed. That is why the `prevState` part of the proposal is especially important. This is how `LanguageCheckboxes` could be simplified with it: ```typescript function LanguageCheckboxes({ additionalLanguages, }: { additionalLanguages: string[]; }) { const clientLanguage = useClientLanguage(); const languages = useMemo( () => new Set([clientLanguage, ...additionalLanguages]), [clientLanguage, additionalLanguages] ); const [checkedLanguages, setCheckedLanguages] = useState<Set<string>>( (prev) => { if (prev === undefined) return new Set(); // If the `languages` set has changed in a way that it no longer includes // some of the languages it included before, those removed languages also // have to be removed from `checkedLanguages`! // Compute the intersection of `languages` and previous `checkedLanguages`: const next = new Set( Array.from(prev).filter((language) => languages.has(language)) ); // Only actually adjust the value if languages have been removed: return next.size < prev.size ? next : prev; }, [languages] ); return /* ... */; } ``` This is very similar to the original `useEffect` solution people love for its readability, but without any of its drawbacks! Isn't that beautiful? By the way, a user-land implementation of this proposal exists, see [`use-state-with-deps`](https://github.com/peterjuras/use-state-with-deps). ## More motivating examples The proposal has been [rejected](https://github.com/facebook/react/issues/14738#issuecomment-461868904) before with the following argumentation: > The idiomatic way to reset state based on props is here: > > https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops > > In other words: > > ```typescript > const [selectedChoice, setSelectedChoice] = useState(options[0]); > const [prevOptions, setPrevOptions] = useState(options); > > if (options !== prevOptions) { > setPrevOptions(options); > setSelectedChoice(options[0]); > } > ``` > > I don't think we want to encourage this pattern commonly so we're avoiding adding a shorter way (although we considered your suggestion). In general, I feel like the React team is trying to make me believe that adjusting state when some input value changes is not something I want to do. I cannot agree with that. Here are 2 examples of real-world scenarios where derived state makes perfect sense that I have encountered just recently: 1. `useRelativeTime`: a hook that returns the relative time string like “in 5 seconds” or “2 minutes ago” that it keeps up-to-date, for a given timestamp, in a given language. The current output value is kept in a state variable that may need to be adjusted when the input timestamp or language changes. <details> <summary>Click to show code</summary> ```typescript import { useEffect, useMemo, useRef, useState } from "react"; import { createRoot } from "react-dom/client"; const units: { name: Intl.RelativeTimeFormatUnit; milliseconds: number }[] = [ { name: "week", milliseconds: 1000 * 60 * 60 * 24 * 7 }, { name: "day", milliseconds: 1000 * 60 * 60 * 24 }, { name: "hour", milliseconds: 1000 * 60 * 60 }, { name: "minute", milliseconds: 1000 * 60 }, { name: "second", milliseconds: 1000 }, ]; function relativeTimeHelper( timeInMs: number, rtf: Intl.RelativeTimeFormat ): readonly [output: string, nextBumpTime: number] { const now = Date.now(); const diff = timeInMs - now; const absDiff = Math.abs(diff); const settleForUnit = (unit: (typeof units)[0]) => { const value = Math.trunc(diff / unit.milliseconds); const output = rtf.format(value, unit.name); const nextBumpTime = diff > 0 // time in the future ? now + (diff % unit.milliseconds) + 1 : timeInMs + (-value + 1) * unit.milliseconds; return [output, nextBumpTime] as const; }; for (const unit of units) { if (absDiff > unit.milliseconds) { return settleForUnit(unit); } } return settleForUnit(units[units.length - 1]); } function useRelativeTime(language: string, timeInMs?: number) { const rtf = useMemo(() => new Intl.RelativeTimeFormat(language), [language]); const [initialOutput, initialNextBumpTime] = useMemo(() => { return timeInMs !== undefined ? relativeTimeHelper(timeInMs, rtf) : []; }, [timeInMs, rtf]); // Does this part have to be so confusing? This would be so much cleaner: // const [output, setOutput] = useState(initialOutput, [initialOutput]); const [output, setOutput] = useState(initialOutput); const [prevInitialOutput, setPrevInitialOutput] = useState(initialOutput); if (initialOutput !== prevInitialOutput) { setPrevInitialOutput(initialOutput); setOutput(initialOutput); } useEffect(() => { if (timeInMs !== undefined) { const bump = () => { const [nextOutput, nextBumpTime] = relativeTimeHelper(timeInMs, rtf); setOutput(nextOutput); timeout = setTimeout(bump, nextBumpTime - Date.now()); }; let timeout = setTimeout(bump, initialNextBumpTime! - Date.now()); return () => clearTimeout(timeout); } }, [timeInMs, rtf, initialNextBumpTime]); return output; } function App() { const time = useRef(Date.now() + 5000); const output = useRelativeTime("en", time.current); return output; } createRoot(document.getElementById("root")!).render(<App />); ``` </details> 2. A component displaying hierarchical data from a tree structure. The components's toolbar includes an input that can be used to control the depth up to which the nodes' children are to be expanded. The data changes dynamically, so it can happen that a previously valid input value becomes invalid because it starts exceeding the overall (maximum) depth of the tree that has decreased. In that case, the input value has to be adjusted to match the new maximum depth. Sure, adjusting state based on input changes is not something you do every day, but nonetheless, scenarios where this is necessary are manifold. I feel like by “avoiding adding a shorter way” so as not “to encourage this pattern”, React does more harm than good: - The developers who don't know about the `prevState` pattern or don't understand it, those who find it too confusing or fail to see its advantages over the `usEffect` approach, as well as those who don't understand how effects work well enough, end up resorting to `useEffect` and introducing inconsistencies that I've illustrated above. This happens so often that I wouldn't be surprised if even Meta's own codebases included examples of this. I am sure that providing a clear API like the suggested enhanced `useState` hook would help reduce the frequency of such misuses of `useEffect`. - The developers who do understand the `prevState` pattern and when to use it end up having to suffer from its deliberate clumsiness or rely on user-land solutions like the aforementioned `use-state-with-deps` library (which, by the way, was not exactly easy to find). This is why I think the proposal should be reconsidered. If I haven't missed anything, it's been more than 5 years since the proposal was last brought up in this repository. The React landscape was very different back then, as the transition from class components to hooks was still ongoing. Maybe it wasn't entirely clear 5 years ago how often the `useEffect` hook would be misused. Also I suppose that developers would resort to class components whenever they wanted to avoid confusing patterns of the new and daunting hook world. This doesn't happen anymore. Taking all of that into consideration, I think the time has come to give the proposal a second look!
This issue appears to be discussing a feature request or bug report related to the repository. Based on the content, it seems to be still under discussion. The issue was opened by aweebit and has received 0 comments.