Hello!
So I've been thinking about something in React for a long time, and I think I have a good opportunity now to explain. Imagine I have an <ExpensiveComponent /> that I want to keep from rendering unnecessarily. I have two options:
sCU, via classes. Here I can directly compare this.props and nextProps for each render cycle:
shouldComponentUpdate(nextProps) {
// compare current props and the very next set of props and return false, if necessary
}
- The areEqual function passed to
React.memo, via function components. This is also the only method I can use if I am using hooks. However, I can't always directly compare current props with next props. Every time I opt out of a render cycle, my 'current props' become stale, making it very difficult to make comparisons that track my normal data flow:
// cycle 1 — comparing props1 and props2 are prevProps and nextProps, as currently listed in the docs
memo(<ExpensiveComponent, (props1, props2) => return true;);
// cycle 2 !!! — !!! comparing props1 and props3 are not prevProps/nextProps! props1 does not get updated to
// props2! comparison is thus much more difficult to make
memo(<ExpensiveComponent, (props1, props3) => return ...);
This part I think is a documentation issue, but I've been struggling with this idea for awhile now, because opting out of successive render cycles is very difficult. Let's say we have a props-driven resource request and no memoization:
// pseudo-ish-code
function Parent() {
var {hasLoaded, isLoading, myData} = useResources(resources, props);
return <ExpensiveComponent {...props} />;
}
As props change to trigger an additional resource request, we have to go through two render cycles:
Render Cycles:
1. props change
2. useResources, via a `useEffect` hook that occurs _after_ the first render, makes the request and
changes `isLoading` to true
With no memoization, ExpensiveComponent is rendered twice every time we fetch new data. But with memoization, it's still pretty difficult to keep ExpensiveComponent from rendering both times since, because of the two render cycles, we can't solely rely on comparing prevProps.isLoading with nextProps.isLoading the way we could with shouldComponentUpdate:
MemoizedExpensiveComponent = memo(<ExpensiveComponent />, areEqual);
// areEqual pass 1. our loading states don't change, so we can't use them to prevent a render
(props1, props2) => {
console.log(props1.isLoading); // false
console.log(props2.isLoading); // false
// both are false, and so ExpensiveComponent will still render
return !props1.isLoading && props2.isLoading;
}
// areEqual pass 2:
(props2, props3) => {
console.log(props2.isLoading); // false
console.log(props3.isLoading); // true
// here we will keep ExpensiveComponent from rendering
return !props2.isLoading && props3.isLoading;
}
For the first cycle, we can compare the changed props that triggered the data request in order to opt-out of the first render, but we have to be careful, because as mentioned previously, we still get props1 in the second areEqual call:
MemoizedExpensiveComponent = memo(<ExpensiveComponent />, areEqual);
// let's say we are fetching new data because the value of `props.query` changed from `'foo'` to `'bar'`.
// areEqual pass 1. compare the value of props.query
(props1, props2) => {
console.log(props1.query); // 'foo'
console.log(props2.query); // 'bar'
// they are unequal, and so ExpensiveComponent will not render this time!
return !props1.query && props2.query;
}
// areEqual pass 2:
(props1, props3) => {
console.log(props1.isLoading); // false
console.log(props3.isLoading); // true
// also here our loading states are not equal, so now we keep ExpensiveComponent from rendering this time, too!!
// except...
console.log(props1.query); // 'foo'
// womp. be careful of this, because now our 'prevProps' is actually not in sync with our data flow.
return !props1.isLoading && props3.isLoading;
}
This is difficult to do in a generalized way, but it is what we ended up doing for the resourcerer library that I work on. And it makes me wonder why, from an API perspective, React ever got rid of componentWillReceiveProps (and never offered an equivalent for hooks)? With cWRP, none of this was an issue, because we only ever had a single render cycle for a new data fetch:
Render Cycles:
1. props change, `isLoading` state is set to true in componentWillReceiveProps prior to render
Here, <ExpensiveComponent /> is at most rendered once per new data fetch, and preventing that render also becomes much easier because we only ever need to compare loading states.
Thank you very much for reading; I know this was long. But I would appreciate any insights into why we can't bring in a 'cWRP-like shortcut' for hooks for this sort of thing, and if I'm missing anything that renders my argument baseless, I really apologize. Otherwise, this is a long post to both (a) see if you would agree that the implementation of areEqual could benefit from more detailed documentation and (b) prompt a discussion about the still-useful merits of componentWillReceiveProps for preventing extra renders.
Hello!
So I've been thinking about something in React for a long time, and I think I have a good opportunity now to explain. Imagine I have an
<ExpensiveComponent />that I want to keep from rendering unnecessarily. I have two options:sCU, via classes. Here I can directly comparethis.propsandnextPropsfor each render cycle:React.memo, via function components. This is also the only method I can use if I am using hooks. However, I can't always directly compare current props with next props. Every time I opt out of a render cycle, my 'current props' become stale, making it very difficult to make comparisons that track my normal data flow:This part I think is a documentation issue, but I've been struggling with this idea for awhile now, because opting out of successive render cycles is very difficult. Let's say we have a props-driven resource request and no memoization:
As props change to trigger an additional resource request, we have to go through two render cycles:
With no memoization,
ExpensiveComponentis rendered twice every time we fetch new data. But with memoization, it's still pretty difficult to keepExpensiveComponentfrom rendering both times since, because of the two render cycles, we can't solely rely on comparingprevProps.isLoadingwithnextProps.isLoadingthe way we could withshouldComponentUpdate:For the first cycle, we can compare the changed props that triggered the data request in order to opt-out of the first render, but we have to be careful, because as mentioned previously, we still get
props1in the secondareEqualcall:This is difficult to do in a generalized way, but it is what we ended up doing for the resourcerer library that I work on. And it makes me wonder why, from an API perspective, React ever got rid of
componentWillReceiveProps(and never offered an equivalent for hooks)? WithcWRP, none of this was an issue, because we only ever had a single render cycle for a new data fetch:Here,
<ExpensiveComponent />is at most rendered once per new data fetch, and preventing that render also becomes much easier because we only ever need to compare loading states.Thank you very much for reading; I know this was long. But I would appreciate any insights into why we can't bring in a 'cWRP-like shortcut' for hooks for this sort of thing, and if I'm missing anything that renders my argument baseless, I really apologize. Otherwise, this is a long post to both (a) see if you would agree that the implementation of
areEqualcould benefit from more detailed documentation and (b) prompt a discussion about the still-useful merits of componentWillReceiveProps for preventing extra renders.