reactjs / reactjs/react.dev

Better explanations for opting out of a render cycle

オープン
#3,678 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

主要言語
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:

  1. 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
}
  1. 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.

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

調査の方向性

issue にリンクされている React.memo のドキュメントから始め、ここで説明されている areEqual の例とレンダーサイクルのシナリオを確認してください。ドキュメントの範囲を明確にし、完了条件を、コンパレータの引数、スキップされるレンダー、および hooks に関する疑問について正確に説明できることと定義してください。この issue には実行するファイルやテストの指定はありません。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
javascript, react
領域
documentation
issue の種類
ドキュメント
難易度
4/5
見積もり時間
3〜5日
活発さ
停滞
明瞭さ
説明が足りない
初心者へのやさしさ
25/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。