Better explanations for opting out of a render cycle
まだ誰も着手していません。
- 主要言語
- JavaScript
- スター
- 11.8k
- フォーク
- 7.9k
- 平均マージ
- 1日 11時間
- マージ済み PR(30日)
- 11
説明
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:
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.
コントリビューションガイド
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
調査の方向性
issue にリンクされている React.memo のドキュメントから始め、ここで説明されている areEqual の例とレンダーサイクルのシナリオを確認してください。ドキュメントの範囲を明確にし、完了条件を、コンパレータの引数、スキップされるレンダー、および hooks に関する疑問について正確に説明できることと定義してください。この issue には実行するファイルやテストの指定はありません。
索引モデルが issue の本文から書いたものです。
評価
- 技術スタック
- javascript, react
- 領域
- documentation
- issue の種類
- ドキュメント
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 活発さ
- 停滞
- 明瞭さ
- 説明が足りない
- 初心者へのやさしさ
- 25/100