Starting with React 16.3, React deprecated some APIs (componentWillMount, componentWillReceiveProps, and componentWillUpdate) and introduced some new ones instead, including getDerivedStateFromProps. getDerivedStateFromProps. Depending on the application scenario, getDerivedStateFromProps is used in different ways.

Semi-controlled components

Although React officially does not recommend semi-controlled components, and certainly not from the perspective of API design and maintenance. However, in practice, users often don’t care about the internal implementation of a business logic, but want to have full control over some internal state when needed, so semi-controlled components are a good choice.

The principle of designing semi-controlled components is to give as much control as possible to the user, i.e., if the user passes a parameter, the user’s parameter is used; if the user does not pass a parameter, the component’s internal state is used. In the past, this could be a bit tricky

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class SomeSearchableComponent extends Component {  
  state = {
    search: props.search || '',
  };

  getSearch() {
    if (typeof this.props.search === 'string') {
      return this.props.search;
    }
    return this.state.search;
  }

  onChange = e => {
    const { onChange } = this.props;
    if (onChange) {
      onChange(e.target.value);
    } else {
      this.setState({
        search: e.target.value,
      });
    }
  };

  render() {
    return <input value={this.getSearch()} onChange={this.onChange} />;
  }
}

A getSearch is encapsulated here, but it cannot be applied to all scenarios, we may have to determine the value on props when getting any operation. Using getDerivedStateFromProps would be more intuitive.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class SomeSearchableComponent extends Component {  
  state = {
    search: props.search || '',
  };

  onChange = e => {
    const { onChange } = this.props;
    if (onChange) {
      onChange(e.target.value);
    } else {
      this.setState({
        search: e.target.value,
      });
    }
  };

  static getDerivedStateFromProps(nextProps) {
    if (typeof nextProps.search === 'string') {
      return {
        search: nextProps.search,
      };
    }
    return null;
  }

  render() {
    const { search } = this.state;
    return <input value={search} onChange={this.onChange} />;
  }
}

Given the design of getDerivedStateFromProps, we can safely synchronize all the values of props to state, so that we only need to fetch the values from state when we use it. Here, we give control to the user as much as possible, as long as the user passes props, the value of props prevails, avoiding the problems caused by the unsynchronized intermediate state.

Note that here we go to determine if the field on props is the type we want (in this case string) instead of determining if the field is on props, because the user may have encapsulated a layer of components that cause the field to be on props, but its value is undefined, e.g.

1
2
3
4
5
6
7
class CustomerComponent extends Component {  
  render() {
    // In the user's component, search is also an optional field, which causes our component `props` to have this field on it, but its value is `undefined`
    const { search } = this.props;
    return <SomeSearchableComponent search={search} />;
  }
}

Components with intermediate states

The second scenario is that some components need to have an intermediate state on user input, and then submit the intermediate result to the upper layer when an action is triggered. Take an input as an example, in the past we used componentWillReceiveProps to synchronize the data to state when the upper level component triggered a redraw.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class SpecialInput extends Component {  
  state = {
    value: this.props.value,
  };

  onChange = e => {
    this.setState({
      value: e.target.value,
    });
  };

  onBlur = e => {
    this.props.onChange(e.target.value);
  };

  componentWillReceiveProps(nextProps) {
    this.setState({
      value: nextProps.value,
    });
  }

  render() {
    const { value } = this.state;
    return (
      <input value={value} onChange={this.onChange} onBlur={this.onBlur} />
    );
  }
}

And the upper component update and the component itself setState will trigger getDerivedStateFromProps, we can know whether this update is triggered by the upper layer or the component itself by comparing whether props is the same object, when props is not the same object, it means that this update comes from the upper component, for example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class SpecialInput extends Component {  
  state = {
    prevProps: this.props,
    value: this.props.value,
  };

  onChange = e => {
    this.setState({
      value: e.target.value,
    });
  };

  onBlur = e => {
    this.props.onChange(e.target.value);
  };

  static getDerivedStateFromProps(nextProps, { prevProps }) {
    if (nextProps !== prevProps) {
      return {
        prevProps: nextProps,
        value: nextProps.value,
      };
    }
    return null;
  }

  render() {
    const { value } = this.state;
    return (
      <input value={value} onChange={this.onChange} onBlur={this.onBlur} />
    );
  }
}

Memoize

Memoize is a simple and common optimization that remembers the result by dirty checking if the value passed in twice is the same. In most cases, it is not recommended to use getDerivedStateFromProps. This can usually be done with a simple helper function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Of course using an array or object and passing in a custom comparison function will allow you to remember multiple parameters
function memorize(func) {  
  let prev;
  let prevValue;
  let init = false;
  return param => {
    if (!init) {
      prevValue = func(param);
      prev = param;
      init = true;
    } else if (!Object.is(prev, param)) {
      prevValue = func(param);
      prev = param;
    }
    return prevValue;
  };
}

class SomeComponent extends Component {  
  getBar = memorize(foo => {
    // ...
  });

  render() {
    const { foo } = this.props;
    const bar = this.getBar(foo);
    // ...
  }
}

And some cases need to use getDerivedStateFromProps, such as a parameter that affects the internal state of the component, when we need to store the previous value on state, e.g.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class SomeComponent extends Component {  
  state = {
    prevType: this.props.type,
    // ...
  };

  static getDerivedStateFromProps({ type }, { prevType }) {
    if (type !== prevType) {
      return {
        prevType: type,
        // ...
      };
    }
    return null;
  }
}

Using Hooks

React 16.8 stabilizes the Hooks API, Hooks has huge advantages over class in many aspects, such as for logic reuse, which is not only more convenient and flexible and intuitive compared to higher-order components, but also has great performance advantages.

For case one, we can achieve this with some helper functions.

1
2
3
4
5
6
7
8
9
function App(props) {  
  const [search, setSearch] = useState('');
  function getSearch() {
    if (typeof props.search === 'string') {
      return props.search;
    }
    return search;
  }
}

For the second case, we can do so with the help of a ref.

1
2
3
4
5
6
7
8
function App(props) {  
  const [value, setValue] = useState('');
  const propsRef = useRef(props);
  if (props !== propsRef.current) {
    setValue(props.value);
    propsRef.current = props;
  }
}

For the third case, you can use useMemo directly.

1
2
3
4
5
function App({ type }) {  
  const computed = useMemo(() => {
    // ...
  }, [type]);
}