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} />;
}
}
|
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]);
}
|