Browse Source

Refactored blog post to include cWRP too

main
Brian Vaughn 6 years ago
parent
commit
22c1a4620e
  1. 104
      content/blog/2018-06-07-you-probably-dont-need-derived-state.md

104
content/blog/2018-06-07-you-probably-dont-need-derived-state.md

@ -3,32 +3,38 @@ title: "You Probably Don't Need Derived State"
author: [bvaughn]
---
React 16.4 included a [bugfix for getDerivedStateFromProps](/blog/2018/05/23/react-v-16-4.html#bugfix-for-getderivedstatefromprops) which made some existing bugs in React components reproduce more consistently. We carefully considered this change, and although we believe it was the right decision, we apologize for any inconvenience it caused. In this post, we will explain some common problems with deriving state, and the preferred solutions to them.
React 16.4 included a [bugfix for getDerivedStateFromProps](/blog/2018/05/23/react-v-16-4.html#bugfix-for-getderivedstatefromprops) which made some existing bugs in React components reproduce more consistently. We carefully considered this change, and although we believe it was the right decision, we apologize for any inconvenience it caused. In this post, we will explain some common anti-patterns with derived state, and the preferred alternatives to each.
The `getDerivedStateFromProps` lifecycle was introduced in 16.3. At the time, we provided [some examples](/blog/2018/03/27/update-on-async-rendering.html#examples) of how to use the new lifecycle to derive state from props. Our primary goal was to help people migrate from legacy lifecycles to newer ones that are safer to use with the [upcoming async rendering mode](blog/2018/03/01/sneak-peek-beyond-react-16.html).
The lifecycle `componentWillReceiveProps` has been around for a long time. Until recently, it was the only way to update state in response to a change in props without triggering an additional render. In version 16.3, [we introduced a replacement lifecycle, `getDerivedStateFromProps`](/blog/2018/03/29/react-v-16-3.html#component-lifecycle-changes), that served the same purpose. However, as we've helped people migrate their components to the new lifecycle, we've uncovered some common anti-patterns for derived state that result in subtle and confusing bugs.
Since that blog post was written, we've observed some common usage patterns for both the legacy `componentWillReceiveProps` and the newer `getDerivedStateFromProps` that cause bugs or otherwise confusing behavior. The 16.4 bugfix [makes this behavior more predictable](https://github.com/facebook/react/issues/12898), but the fixes aren't always obvious. This blog post will guide you through those fixes.
> Note
>
> The anti-patterns described in this post apply to both `componentWillReceiveProps` and `getDerivedStateFromProps`!
In 16.4, we released a bugfix that [makes derived state behavior more predictable](https://github.com/facebook/react/issues/12898) so the results of misusing it are easier to notice. This blog post will guide you through when derived state should be used, and when there are better alternatives.
Let's take a look at the following topics:
We'll cover the following topics:
* [When should I use derived state?](#when-should-i-use-derived-state)
* [When should I avoid derived state?](#when-should-i-avoid-derived-state)
* [Anti-pattern: Mirroring props in state](#anti-pattern-mirroring-props-in-state)
* [Anti-pattern: Erasing state when props change](#anti-pattern-erasing-state-when-props-change)
* [What about memoization?](#what-about-memoization)
## When should I use derived state?
`getDerivedStateFromProps` exists for only one purpose. It enables a component to update its internal state as the result of **changes in external props**. This distinction is important. The problems we've seen so far can be ultimately reduced to either (1) unconditionally updating state from props or (2) updating state whenever props and state don't match. (We'll go over both in more detail below.)
`getDerivedStateFromProps` exists for only one purpose. It enables a component to update its internal state as the result of **changes in props**. This distinction is important. All problems with derived state that we have seen can be ultimately reduced to either (1) unconditionally updating state from props or (2) updating state whenever props and state don't match. (We'll go over both in more detail below.)
So what does it mean to update state as the result of changes in props? Our previous blog post provided a few examples, like [managing the current scroll direction based on an offset prop](/blog/2018/03/27/update-on-async-rendering.html#updating-state-based-on-props) or [loading external data specified by a source prop](/blog/2018/03/27/update-on-async-rendering.html#fetching-external-data-when-props-change).
So what does it mean to update state as the result of changes in props? Our previous blog post provided some examples, like [managing the current scroll direction based on an offset prop](/blog/2018/03/27/update-on-async-rendering.html#updating-state-based-on-props) or [loading external data specified by a source prop](/blog/2018/03/27/update-on-async-rendering.html#fetching-external-data-when-props-change). We did not provide many examples, because as a general rule, **derived state should be used sparingly**.
As a general rule, if you're not sure about whether you should use `getDerivedStateFromProps`, here are some questions to ask yourself:
If you're not sure about whether your component should use derived state, here are some questions to ask yourself:
* Is the state **derived** from props (as opposed to just mirroring it)?
* Is the state update specifically triggered by a **props change** (and not just the current props value)?
If your answer to either of the above questions is "no" then there are better patterns to use.
If your answer to either of the above questions is "no" then there are better patterns to use! (We'll cover them below.)
## When should I avoid derived state?
The terms ["controlled"](/docs/forms.html#controlled-components) and ["uncontrolled"](/docs/uncontrolled-components.html) are often used to refer to form components, but they can also be used to describe where a component's state lives. Data that is passed in as props can be thought of as **controlled** (because the parent component _controls_ that data). Data that exists only in internal state can be thought of as **uncontrolled** (because the parent can't directly change it).
The terms ["controlled"](/docs/forms.html#controlled-components) and ["uncontrolled"](/docs/uncontrolled-components.html) are often used to refer to form components, but they can also be used to describe where a component's data lives. Data that is passed in as props can be thought of as **controlled** (because the parent component _controls_ that data). Data that exists only in internal state can be thought of as **uncontrolled** (because the parent can't directly change it).
The most common mistake with derived state is mixing these two. In other words, when props control a value in state that is also updated by `setState` calls. This may sound similar to the [external data loading example](/blog/2018/03/27/update-on-async-rendering.html#fetching-external-data-when-props-change) mentioned above, but it's different in a few important ways.
@ -36,25 +42,25 @@ In the loading example, there is a clear source of truth for both the "source" p
Problems arise when any of these constraints are changed. This typically comes in two forms. Let's take a look at both.
### Anti-pattern: Props always override state
### Anti-pattern: Mirroring props in state
A common misconception about both `getDerivedStateFromProps` and `componentWillReceiveProps` is that they are only called when props "change". However, any time a parent component re-renders, the child component still receives the new props. Because of this, it is (and has always been) unsafe to _unconditionally_ override state values using either of these lifecycles.
A common misconception is that `getDerivedStateFromProps` and `componentWillReceiveProps` are only called when props "change". However, these lifecycles will be called any time a parent component re-renders, regardless of props changes. Because of this, it is (and has always been) unsafe to _unconditionally_ override state using either of these lifecycles. **Doing so will cause state updates to be lost.**
Before React 16.4, it was difficult to reproduce such bugs consistently because they depend on the timing of parent re-rendering. To expose them consistently, we made `getDerivedStateFromProps` in React 16.4 fire for renders caused by `setState` or `forceUpdate` too. Let’s consider an example to demonstrate the problem.
This was less obvious before React 16.4, because the behavior depended on when a parent component re-rendered. This made it possible to overlook potential bugs when testing locally. To make things more consistent and predictable, we made `getDerivedStateFromProps` fire for renders caused by `setState` and `forceUpdate` too.
Here is a `EmailInput` component that accepts an email prop:
Let’s consider an example to demonstrate the problem. Here is a `EmailInput` component that "mirrors" an email prop in state:
```js
class EmailInput extends Component {
state = {
email: this.props.email
};
static getDerivedStateFromProps(props, state) {
// This is bad! Do not do this!
if (props.email) {
return { email: props.email };
componentWillReceiveProps(nextProps) {
// This will erase any local state updates!
// Do not do this!
if (nextProps.email) {
this.setState({ email: nextProps.email });
}
return null;
}
handleChange = event => {
@ -67,11 +73,11 @@ class EmailInput extends Component {
}
```
At first, this component might look okay. State is initialized to the value specified by props and updated when we type into the `<input>`. But once our component re-renders— either because it called `setState` or because its parent re-rendered— anything we've typed into the `<input>` will be lost! ([See this demo for an example.](https://codesandbox.io/s/m3w9zn1z8x))
At first, this component might look okay. State is initialized to the value specified by props and updated when we type into the `<input>`. But if our component's parent re-renders, anything we've typed into the `<input>` will be lost! ([See this demo for an example.](https://codesandbox.io/s/m3w9zn1z8x))
At this point, you might be wondering if this component would have worked with version 16.3. Unfortunately, the answer is "no". Before moving on, let's take a look at why this is.
We could use `shouldComponentUpdate` to ensure that our component did not re-render unless props changed. This would fix the simple component showed above. However in practice, components usually accept multiple props, and our component would re-render if any one of them changed— not just email.
For the simple example above, we could "fix" the problem of unexpected re-renders using `shouldComponentUpdate`. However in practice, components usually accept multiple props, and our component would re-render if any one of them changed. Even if none of them "changed", props that are functions or objects are often created inline and so will always bypass `shouldComponentUpdate`. For example, what if our component accepted a function to validate the current email address?
Another thing to to keep in mind is function or object props are often created inline and so will always bypass `shouldComponentUpdate`. For example, what if our component accepted a function to validate the current email address?
```js
<EmailInput
email={this.props.user.email}
@ -79,45 +85,46 @@ For the simple example above, we could "fix" the problem of unexpected re-render
/>
```
The above example binds the validation callback inline and so it will pass a new function prop every time it renders— effectively bypassing `shouldComponentUpdate` entirely. Even before `getDerivedStateFromProps` was introduced, this exact pattern led to bugs in `componentWillReceiveProps`. [Here is another demo that shows it.](https://codesandbox.io/s/jl0w6r9w59)
This example binds the validation callback inline and so it will pass a new function prop every time it renders— effectively bypassing `shouldComponentUpdate` entirely. ([Here is a demo that shows that happening.](https://codesandbox.io/s/jl0w6r9w59))
Hopefully it's clear by now why unconditionally overriding state with props is a bad idea. But what if we were to only update the state when the email prop changes? We'll take a look at that pattern next.
Hopefully it's clear by now why **it is a bad idea to unconditionally mirror props in state**. But what if we were to only update the state when the email prop changes? We'll take a look at that pattern next.
### Anti-pattern: Props override state whenever they change
### Anti-pattern: Erasing state when props change
Building on our example above, we could avoid accidentally erasing state by only updating it when `props.email` changes:
Continuing the example above, we could avoid accidentally erasing state by only updating it when `props.email` changes:
```js
class EmailInput extends Component {
state = {
prevPropsEmail: this.props.email,
email: this.props.email
};
static getDerivedStateFromProps(props, state) {
componentWillReceiveProps(nextProps) {
// Any time props.email changes, update state.
if (props.email !== state.prevPropsEmail) {
return {
prevPropsEmail: props.email,
email: props.email
};
if (nextProps.email !== this.props.email) {
this.setState({
email: nextProps.email
});
}
return null;
}
// ...
}
```
We've just made a big improvement. Now our component will no longer erase what we've typed every time it renders. At this point, what we have is fairly similar to the [scroll direction example](blog/2018/03/27/update-on-async-rendering.html#updating-state-based-on-props) we mentioned before. (It will also more closely mirror how `componentWillReceiveProps` would have been used in the past.)
> Note
>
> Even though the example above shows `componentWillReceiveProps`, the same anti-pattern applies to `getDerivedStateFromProps`.
We've just made a big improvement. Now our component will no longer erase what we've typed when it is re-rendered. At this point, what we have is fairly similar to the [scroll direction example](blog/2018/03/27/update-on-async-rendering.html#updating-state-based-on-props) we mentioned before.
There is still one subtle problem though, and it's probably easiest to illustrate with an example. Let's say our `EmailInput` component is used inside of an "edit profile" form. The first time the form is rendered, the component will display our email address. Let's say we edited the form (including our email address) but then changed our mind and clicked a "reset" button. At this point, we would expect all form fields to return to their initial values— but the `EmailInput` component **will not be reset**. Do you know why?
The reason for this is that `props.email` never actually changed in the above scenario. Both times the "edit profile" form rendered, it would pass our saved email address via props.
The reason for this is that `props.email` never actually changed in the above scenario. Both times the "edit profile" form rendered, it would pass the saved email address via props.
This problem could manifest itself even without a "reset" button. For example, imagine a password manager app using the above input component. When navigating between details for two accounts with the same email, the input would fail to reset. This is because the prop value passed to the component would be the same for both accounts! This would be a surprise to the user, as a draft change to one account would appear to affect other accounts that happened to share the same email.
This design is fundamentally flawed, but it's also an easy mistake to make. [I've made it myself.](https://twitter.com/brian_d_vaughn/status/959600888242307072) Fortunately there are two alternatives that work better. The key to both is that **for any piece of state, you need to pick a single component that owns it as the source of truth, and avoid duplicating it in other components.** Let's take a look at each of the alternatives.
This design is fundamentally flawed, but it's also an easy mistake to make. [I've made it myself.](https://twitter.com/brian_d_vaughn/status/959600888242307072) Fortunately there are two alternatives that work better. The key to both is that **for any piece of data, you need to pick a single component that owns it as the source of truth, and avoid duplicating it in other components.** Let's take a look at each of the alternatives.
#### Alternative 1: Fully controlled component
@ -128,7 +135,7 @@ function EmailInput(props) {
}
```
This approach simplifies the implementation of our component but it also has a potential downside: our component now requires more effort to use. For example, the parent form component will now also need to manage local (unsaved) email state.
This approach simplifies the implementation of our component but it also has a potential downside: our component now requires more effort to use. For example, the parent form component will now also need to manage "draft" (unsaved) email state.
#### Alternative 2: Fully uncontrolled component
@ -165,7 +172,9 @@ One approach would be to use a special React attribute called `key`. React uses
> Note
>
> With this approach, you don't have to add `key` to every input. Instead, it might make sense to put a `key` on the whole form. Every time the key changes, all components within the form will be re-created with a freshly initialized state. While this may sound slow, in practice the difference is often insignificant. It can even be faster if the components have heavy logic that runs on updates.
> With this approach, you don't have to add `key` to every input. It might make more sense to put a `key` on the whole form instead. Every time the key changes, all components within the form will be re-created with a freshly initialized state.
>
> While this may sound slow, in practice the difference is often insignificant. It can even be faster if the components have heavy logic that runs on updates.
#### Option 2: Reset uncontrolled component with an id prop
@ -197,6 +206,10 @@ class EmailInput extends Component {
}
```
> Note
>
> Even though the example above shows `getDerivedStateFromProps`, the same technique can be used with `componentWillReceiveProps`.
This approach can scale better with multiple state values, since fewer comparisons are required. It also provides the flexibility to only reset parts of our component's internal state.
#### Option 3: Reset uncontrolled component with an instance method
@ -238,7 +251,7 @@ For **uncontrolled** components, if you're trying to **"reset" one or more state
We've also seen derived state used to ensure an expensive value is recomputed only when the inputs change. This technique is known as [memoization](https://en.wikipedia.org/wiki/Memoization).
Using derived state for memoization isn't necessarily bad, but there are reasons you may want to avoid it. Hopefully the above examples illustrate that there is a certain amount of complexity inherent in using `getDerivedStateFromProps`. This complexity increases with each derived property. For example, if we add a second derived field to our component state then our implementation would need to separately track changes to both:
Using derived state for memoization isn't necessarily bad, but there are reasons you may want to avoid it. The above examples illustrate that there is a certain amount of complexity inherent in managing derived state. This complexity increases with each derived property. For example, if we add a second derived field to our component state then our implementation would need to separately track changes to both. For `getDerivedStateFromProps`, this might look like:
```js
class Example extends Component {
@ -262,9 +275,9 @@ class Example extends Component {
}
```
Although the above pattern _works_, it isn't what `getDerivedStateFromProps` is meant to be used for. This component isn't reacting to **changes in props**. It's just computing values based on the **current props**, and there is a simpler way to do this that doesn't even require state.
Although the above pattern _works_, it isn't what either `componentWillReceiveProps` or `getDerivedStateFromProps` were meant to be used for. This component isn't reacting to **changes in props**. It's just computing values based on the **current props**, and there is a simpler way to do this that doesn't even require state.
Let's look at another example to illustrate this. Here is a component that accepts an array of items as `props.list` and allows a user to filter the array by entering text. We could use `getDerivedStateFromProps` for this purpose:
Let's look at another example to illustrate this. Here is a component that accepts an array of items as `props.list` and allows a user to filter the array by entering text. We could use derived state to store the filtered list:
```js
class Example extends Component {
@ -361,6 +374,9 @@ class Example extends Component {
};
render() {
// Get the filtered list for the current values.
// If those values haven't changed since the last render,
// The memoization helper will return the previous filtered list.
const filteredList = this.filter(this.props.list, this.state.filterText);
return (
@ -373,16 +389,18 @@ class Example extends Component {
}
```
This is much simpler and performs just as well as the previous version!
This is much simpler and performs just as well as the derived state version!
There are a couple of constraints to consider when using memoization though:
1. In most cases, you'll want to **attach the memoized function to a component instance**. This prevents multiple instances of a component from resetting each other's memoized keys.
1. Typically you'll want to use a memoization helper with a **limited cache size** in order to prevent memory from "leaking" over time. (In the example above, we used `memoize-one` because it only caches the most recent argument and result.)
1. Memoization will not work properly if `props.items` is recreated each time the parent component renders. Hopefully, this should not be the case for large arrays. If it were, you'd want to address that problem first! (This limitation also applies to the `getDerivedStateFromProps` version above.)
1. Memoization will not work properly if `props.items` is recreated each time the parent component renders. Hopefully, this should not be the case for large arrays. If it were, you'd want to address that problem first! (This limitation also applies to the derived state versions above.)
## In closing
The examples above are intentionally simplified in order to highlight specific coding patterns. In real world applications, components often contain a mix of controlled and uncontrolled behaviors. This is okay! Just be careful to ensure that each behavior has a clear source of truth in order to avoid the anti-patterns mentioned above.
It is also worth re-iterating that `getDerivedStateFromProps` (and derived state in general) should be used sparingly because of the inherent complexity involved.
If you have a use case that you think falls outside of these patterns, please share it with us on [GitHub](https://github.com/reactjs/reactjs.org/issues/new) or Twitter!

Loading…
Cancel
Save