Search code examples
reactjsreact-hooksgetderivedstatefromprops

React Hooks FAQ: implementation of getderivedstatefromprops leads to rendering with incoherent state


IMHO The suggestion from React Hooks FAQ #getDerivedStateFromProps leads to one first rendering with a value of row that doesn't correspond to the value of isScrollingDown . As the call to setIsScrollingDown only schedules a new rendering and doesn't affect the current rendering, the latter will be executed with the new value of row and the old value isScrollingDown.

This behaviour is not equivalent to the static getderivedstatefromprops method of the component class that allows coherence between row and isScrollingDown.

Should not the example be updated with something like the following code in order to guarantee a coherent rendering ? Or did I miss something ?

Thank you !

function ScrollView({row}) {
    let [isScrollingDown, setIsScrollingDown] = useState(false);
    let [prevRow, setPrevRow] = useState(null);

    if (row !== prevRow) {
        // Row changed since last render. Update isScrollingDown.
        isScrollingDown = prevRow !== null && row > prevRow
        setIsScrollingDown(isScrollingDown);
        setPrevRow(row);
    }

    return `Scrolling down: ${isScrollingDown}`;
}


Solution

  • Here's the important part from the documentation that makes your change unnecessary:

    React will re-run the component with updated state immediately after exiting the first render so it wouldn’t be expensive.

    The render where they are out of sync will never be committed to the browser. In fact, if it was returning a child component from the render, the rendering of the children would not execute until after the state has been updated (the children returned from the render that updated the state would be ignored).

    Below is an example with console logs added to show this. Notice that when you increment the row, ScrollView renders twice but ScrollingDown only renders once receiving only the last version of ScrollView's state.

    import React, { useState } from "react";
    import ReactDOM from "react-dom";
    
    function ScrollingDown({ isScrollingDown, prevRow, row }) {
      console.log("ScrollingDown", isScrollingDown, prevRow, row);
      return (
        <div>
          {`Scrolling down: ${isScrollingDown}`}
          <br />
          {`prevRow: ${prevRow}`}
          <br />
          {`row: ${row}`}
        </div>
      );
    }
    
    function ScrollView({ row }) {
      let [isScrollingDown, setIsScrollingDown] = useState(false);
      let [prevRow, setPrevRow] = useState(null);
    
      if (row !== prevRow) {
        // Row changed since last render. Update isScrollingDown.
        setIsScrollingDown(prevRow !== null && row > prevRow);
        setPrevRow(row);
      }
      console.log("ScrollView", isScrollingDown, prevRow, row);
      return (
        <ScrollingDown
          isScrollingDown={isScrollingDown}
          prevRow={prevRow}
          row={row}
        />
      );
    }
    
    function App() {
      const [row, setRow] = useState(1);
      return (
        <div className="App">
          <ScrollView row={row} />
          <button onClick={() => setRow(prev => prev + 1)}>Increment Row</button>
        </div>
      );
    }
    
    const rootElement = document.getElementById("root");
    ReactDOM.render(<App />, rootElement);
    

    Edit Hooks getDerivedStateFromProps