React and componentDidReceiveProps
April 2, 2016
react
programming
Thesis:
Is there a case to be made for React setting props
and state
earlier in the component lifecycle?
Do you have this boilerplate in your React class?
var Thing = React.createClass({
...
componentDidMount() {
this.loadUserPosts(this.props);
},
// Detect and react to changes in the userID prop
componentWillReceiveProps(nextProps) {
if (this.props.userID !== nextProps.userID) {
this.loadUserPosts(nextProps);
}
},
// Loads all posts for the current user
loadUserPosts: function(props) {
... load all posts for props.userID
}
...
});
If so, you are not the only one.
READMORE
Look closely at why loadUserPosts()
takes a props
parameter.
This boilerplate is common because usually you want to fetch your data when your component is mounted (componentDidMount
) and whenever a specific prop changes (componentWillReceiveProps
). In this pattern, the key items in those methods are this.props
and nextProps
respectively and because of that difference, we need to create a method which takes a parameter creating the ugly boilerplate.
Let’s look quickly at the component lifecycle methods as defined by React:
function componentWillReceiveProps(nextProps) {...}
function shouldComponentUpdate(nextProps, nextState) {...}
function componentWillUpdate(nextProps, nextState) {...}
function render() {...}
function componentDidUpdate(prevProps, prevState) {...}
The component lifecycle methods in React mostly operate on the principle that your component wants to anticipate a pending change to state and props. It’s not until the render()
method that this.props
and this.state
are updated to the new values.
If you have a lot of different methods that react to changes in props, you might have written a lot of these methods that accept a props
parameter. Github React Issue #3279 requests a componentDidReceiveProps
precisely to remove that annoying boilerplate.
Enter componentDidReceiveProps()
As a thought experiment, let’s imagine we have a method from React called componentDidReceiveProps(prevProps)
in which props had already assigned to this
:
var Thing = React.createClass({
...
componentDidMount() {
- this.loadUserPosts(this.props);
+ this.loadUserPosts();
},
// Detect and react to changes in the userID prop
- componentWillReceiveProps(nextProps) {
- if (this.props.userID !== nextProps.userID) {
- this.loadUserPosts(nextProps);
+ componentDidReceiveProps(prevProps) {
+ if (this.props.userID !== prevProps.userID) {
+ this.loadUserPosts();
}
},
// Loads all posts for the current user
- loadUserPosts: function(props) {
- ... load all posts for props.userID
+ loadUserPosts: function() {
+ ... load all posts for this.props.userID
}
...
});
This all looks good except that we have deeper problem: shouldComponentUpdate
will be called after componentDidReceiveProps
and shouldComponentUpdate
keeps the paradigm of anticipating change in the nextProps
, nextState
parameters. In shouldComponentUpdate
, this.state/props
is still the “old” state and props.
Would we want React to assign the new state and props to this
for componentDidReceiveProps
just to reassign the old values for shouldComponentUpdate
? And not only that, but there is still componentWillUpdate
to call before render
.
We’d need to change more.
What would life be like if React’s lifecycle methods had already set this.state/props
?
To continue with the experiment, let’s see what it looks like if React assigns values to this.state/props
for every lifecycle method and passes in the old values.
var Thing = React.createClass({
componentDidReceiveProps: function(prevProps) {
// ...
},
shouldComponentUpdate: function(prevProps, prevState) {
// ...
},
componentWillUpdate: function(prevProps, prevState) {
// ...
},
render: function() {
// ...
}
});
With these in place, it’s still possible to compare the old vs, new props, but this.props/state
is now at the state it is in render
.
This is all fun to think about, but what exactly would the impact be of changing it? I don’t know, but I looked through all of the React code in the office and this is what I found:
| Total
———————————————|————-
Total number of components | 297
With componentWillReceiveProps
| 40
With componentWillReceiveProps
boilerplate | 19
- For
shouldComponentUpdate
, no component did more than compare the current props with the next props. - Only one component implemented
componentWillUpdate
with more than a simple comparison and any methods called from it, had to take a parameter fornextState/Props
, suffering the same boilerplate ascomponentWillReceiveProps
.
Using a different paradigm for fun
If you are curious to experiment with a different implementation, here is a JSFiddle that contains a v2
function in which you can wrap your React class definition.
Final notes
This hack only applies to the “conventional” way of creating React classes. An ES6 solution is left as an exercise for the reader (don’t you just love that writer’s escape hatch!).
Additionally, there is no guarantee this is safe to do. Certainly it’s worth investigating the React source code to see how dangerous such a “hack” is.