reactjs / reactjs/react.dev

Better explanations for opting out of a render cycle

Offen
#3,678 1 Kommentar 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Dieses Issue hat noch niemand übernommen.

Vorherrschende Sprache
JavaScript
Sterne
11.8k
Forks
7.9k
Ø Merge
1 T. 11 Std.
Gemergte PRs (30 T.)
11

Beschreibung

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.

Beitragsleitfaden

Beitragsleitfaden öffnen

Erste Schritte

  1. Lies das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreib ins Issue, dass du es übernimmst — das erspart doppelte Arbeit.
  3. Forke das Repository und arbeite in einem Branch.
  4. Öffne einen Pull Request, der die Issue-Nummer nennt.

Rechercherichtung

Beginne mit der in der Issue verlinkten Dokumentation zu React.memo und prüfe die hier beschriebenen areEqual-Beispiele und Szenarien für Render-Zyklen. Kläre den Umfang der Dokumentation und definiere den Abschluss als eine korrekte Erklärung der Comparator-Argumente, übersprungener Render-Vorgänge und der Frage im Zusammenhang mit Hooks; die Issue nennt keine Datei und keinen auszuführenden Test.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

Tech-Stack
javascript, react
Bereich
documentation
Issue-Typ
Dokumentation
Schwierigkeit
4/5
Geschätzter Aufwand
3-5 Tage
Aktivitätsstatus
Veraltet
Klarheit
Muss geklärt werden
Anfängerfreundlichkeit
25/100

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.