Original link: reactjs.org/blog/2018/0…

I translated this article because the lifecycle of getDerivedStateFromProps was incorrectly used during a requirements iteration, causing the state of the child component to be reset in a loop.


For a long time, life cycle componentWillReceiveProps response props are used to update to the only way to change the state does not need additional rendering. In version 16.3, we provided a more secure lifecycle of getDerivedStateFromProps to solve the same use case. At the same time, we found a lot of misunderstanding about how to use these two approaches, and we found some subtle and confusing anti-patterns. A bug fix for getDerivedStateFromProps in 16.4 made derived state more predictable and made it easier to notice the consequences of using it incorrectly.

When to use derived state

GetDerivedStateFromProps exists for only one purpose. It allows components to update internal state based on changes in props. The blog between us provides examples of changing a prop for offset to change the current scrolling direction and loading external data specified by source props.

We do not provide further examples, because as a general rule, derived states should be used with caution. All derived state causes the same problem as (1) updating state unconditionally against props (2) updating state regardless of whether the props and state match.

  • If only derived state is used to record some calculations based on the current props, derived state is not required;
  • If you update derived state unconditionally, or update state regardless of whether props and state match, your component will reset state too often;

Common problems with derived state

“Controlled” and “uncontrolled” are commonly used to refer to form input, but it can also refer to the location of any component data. Data passing through props is considered “controlled” (because the parent component is controlling this data). Data that exists only in its internal state is considered “uncontrolled” (because its parent cannot directly alter it).

The most common mistake with derived states is to mix the two together. When the value of a derived state is also updated by a call to setState, there is no guarantee that the data has a single true source. This may be similar to the external data loading example mentioned above, but they differ in some important respects. In the loading example, both the props of “source” and the state of “loading” have an explicit true source. When the source props changes, the loading state should always be overridden. Instead, override state only when the props changes and is managed by the component.

Problems arise when any of these constraints are changed. There are usually two forms, so let’s look at these two forms.

Anti-pattern: Unconditionally copy state from prop to State

A common misconception is getDerivedStateFromProps and componentWillReceiveProps only at the time of props change will be invoked. Both life cycles will be called whenever the parent component is re-rendered, regardless of whether the props are different from before. Therefore, overwriting state unconditionally is always unsafe when using both life cycles and will result in the loss of state updates.

Let’s consider an example to illustrate the problem.

class EmailInput extends Component {
  state = { email: this.props.email };

  render() {
    return<input onChange={this.handleChange} value={this.state.email} />; } handleChange = event => { this.setState({ email: event.target.value }); }; ComponentWillReceiveProps (nextProps) {/ / here will override any update of the local state. this.setState({ email: nextProps.email }); }}Copy the code

This component may look fine, state is initialized by data passed in from prop, and state is updated when we change the value of the input. But when our parent component rerenders, any state we entered in the input will be lost, even if we compare nextProps. Email! == this.state.email.

In this example, only adding shouldComponentUpdate to re-render when the email prop changes can solve this problem, but in practice, components often receive multiple prop, and changes to one prop can still cause component re-render and incorrect reset. In addition, functions and object prop are often created inline, which can also make it difficult for shouldComponentUpdate to return true correctly. Here’s an example. Therefore shouldComponentUpdate is usually used for performance optimization rather than to determine the correctness of derived state.

Hopefully by now it’s clear why you don’t want to copy props to state unconditionally. Before we find a possible solution, let’s look at a related problem: what if state was updated only when props. Email changed?

Antipattern: Clears state when props changes

Continuing the example above, we can avoid accidentally clearing state when the props. Email changes:

class EmailInput extends Component { state = { email: this.props.email }; ComponentWillReceiveProps (nextProps) {/ / props at any time. The email change, update the state.if (nextProps.email !== this.props.email) {
      this.setState({
        email: nextProps.email
      });
    }
  }
  
  // ...
}
Copy the code

We’ve made a lot of progress, and now our component only clears state when the props actually changes.

As a subtle matter, imagine building a password management application using the above components. When the same email is used to navigate the detail pages of both accounts, the input cannot be reset because the props passed to the component are the same relative to both accounts. This will come as a pleasant surprise to users, as unsaved changes to one account can mistakenly affect another. Check out the demo.

This design is inherently wrong, but it’s an easy mistake to make. Fortunately, there are two better alternatives. The key to both is that for any piece of data, you need to choose a component that uses it as a data source and avoid reusing it in other components.

The preferred solution

Recommended: Fully controlled components

One way to avoid this problem is to remove state from the build entirely. If email was only used as props, we wouldn’t have to worry about it clashing with state, and we could even make the EmailInput component a more lightweight function component:

function EmailInput(props) {
  return <input onChange={props.onChange} value={props.email} />;
}
Copy the code

This approach simplifies component implementation, but if you still need to store a draft value, the parent form component now needs to do that manually. Check out the demo.

Recommended: Completely uncontrolled components with keys

The alternative is to give our component full “draft” email state, in which case our component can still receive props as the initial value, but it ignores subsequent changes to props.

class EmailInput extends Component {
  state = { email: this.props.defaultEmail };

  handleChange = event => {
    this.setState({ email: event.target.value });
  };

  render() {
    return<input onChange={this.handleChange} value={this.state.email} />; }}Copy the code

To reset the value when moving to another project (such as in a password manager scenario), you can use a special property key of React. When a key changes, React creates a new component instance instead of updating the current component. Keys are usually used in dynamic lists but can also be used here.

<EmailInput
  defaultEmail={this.props.user.email}
  key={this.props.user.id}
/>
Copy the code

Whenever the ID changes, the EmailInput component will be recreated and its state will be reset to the value of the last defaultEmail. Check out the demo. With this method, instead of adding a key to each input, it makes more sense to place a key on the entire form. Each time the key changes, the input in the form is reset to its original state.

Alternative 1: Reset uncontrolled components with ID Prop

If the key is not applicable in some cases (perhaps initialization is expensive for the component), a possible but tedious way to monitor the userID is in getDerivedStateFromProps:

class EmailInput extends Component {
  state = {
    email: this.props.defaultEmail,
    prevPropsUserID: this.props.userID
  };

  static getDerivedStateFromProps(props, state) {
    if(props.userID ! == state.prevPropsUserID) {return {
        prevPropsUserID: props.userID,
        email: props.defaultEmail
      };
    }
    return null;
  }

  // ...
}
Copy the code

This also provides the flexibility to reset only part of the state within the component if we choose. Check out the demo.

Alternative 2: Use instance methods to reset uncontrolled components

If there is no suitable ID for the key but the state needs to be reset, one solution is to generate a random number or auto-increment value for the key for the component, or an instance method to force the state of the component to be reset.

class EmailInput extends Component {
  state = {
    email: this.props.defaultEmail
  };

  resetEmailForNewUser(newEmail) {
    this.setState({ email: newEmail });
  }

  // ...
}
Copy the code

The parent component calls this method by getting the instance of the component through ref. Check out the demo.

Ref can be useful in some scenarios, but we recommend that you use it with caution, even in the demo, this method is the least desirable as it will result in two renders instead of one.

conclusion

In summary, when designing a component, an important aspect is whether its data is controllable or uncontrolled.

Try to avoid “mirroring” a props value in state, make this component a controlled component, and merge the two states in the parent component’s state. For example, instead of accepting a committed props and tracking a draft state in a component, let the parent component manage both the state.draftValue and state.committedValue and directly control the child components. This will make components more explicit and predictable.

For an uncontrolled component, if you want to reset state based on a change in props, you need to follow these guidelines:

  • Preferred: To reset all internal states, usekeyProperties;
  • Option 1: Monitor changes in properties in props if only part of the state is reset;
  • Option 2: You can still consider passingrefMethod of invoking power;

Memoization?

We’ve also seen derived states used to ensure that expensive values used in rendering are recalculated only if the input changes, a technique called memoization

Using derived states to do memoization isn’t necessarily a bad thing, but it’s usually not the best solution. There is some complexity in managing derived state, and this complexity increases with the increase in attributes. For example, if we added a second derived field to the component’s state, our implementation would need to track changes to both fields separately.

Let’s look at an example of a component that uses a PROP (list of items) and renders items that match the search query entered by the user. We can use derived state to store filter lists:

class Example extends Component {
  state = {
    filterText: ""}; // ******************************************************* // NOTE: this example is NOT the recommended approach. // See the examples belowfor our recommendations instead.
  // *******************************************************

  static getDerivedStateFromProps(props, state) {
    // Re-run the filter whenever the list array or filter text change.
    // Note we need to store prevPropsList and prevFilterText to detect changes.
    if( props.list ! == state.prevPropsList || state.prevFilterText ! == state.filterText ) {return {
        prevPropsList: props.list,
        prevFilterText: state.filterText,
        filteredList: props.list.filter(item => item.text.includes(state.filterText))
      };
    }
    return null;
  }

  handleChange = event => {
    this.setState({ filterText: event.target.value });
  };

  render() {
    return( <Fragment> <input onChange={this.handleChange} value={this.state.filterText} /> <ul>{this.state.filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul> </Fragment> ); }}Copy the code

This approach avoids recalculating the filteredList. But it was more complex than we needed, because it required tracking and checking our props and state separately to update the list correctly. In the following example, we simplify by PureComponent and putting the filter action in render:

// PureComponents will only re-render if at least one state or prop changes // Confirm the change by shallow comparison of the keys of state and props. class Example extends PureComponent { state = { filterText:""
  };

  handleChange = event => {
    this.setState({ filterText: event.target.value });
  };

  renderConst filteredList = this.list.filter () {// PureComponent render calls const filteredList = this.list.filter (item) only when props. List or state.filterText is changed => item.text.includes(this.state.filterText) )return( <Fragment> <input onChange={this.handleChange} value={this.state.filterText} /> <ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul> </Fragment> ); }}Copy the code

The above example is cleaner and more concise than the derived version, but sometimes this may not be good enough, such as filtering can be slow for large lists and not prevent the PureComponent from being re-rendered if other props change it. To address both of these issues, we can add an memoization to avoid unnecessarily re-filtering our list:

import memoize from "memoize-one";

class Example extends Component {
  state = { filterText: "" };

  filter = memoize(
    (list, filterText) => list.filter(item => item.text.includes(filterText))
  );

  handleChange = event => {
    this.setState({ filterText: event.target.value });
  };

  render() {
    const filteredList = this.filter(this.props.list, this.state.filterText);

    return( <Fragment> <input onChange={this.handleChange} value={this.state.filterText} /> <ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul> </Fragment> ); }}Copy the code

When using Memoization, there are the following constraints:

  • In most cases, you need to attach the Memoized function to the component instance. This prevents multiple instances of the component from resetting each other’s Memoized keys.
  • Typically, you need to use Memoization with a limited cache size to prevent memory leaks. (In the example above, we used Memoize-One because it caches only the most recent parameters and results.)
  • The implementation shown in this section would not work if you recreated the props. List each time the parent component was rendered. But for the most part, this setup is appropriate.

The last

In practice, components often contain a mixture of controlled and uncontrolled behavior. Never mind, if each value has a clear source, you can avoid the anti-pattern mentioned above.

It’s worth rethinking that getDerivedStateFromProps (and derived state in general) is an advanced feature that should be used with caution.