Search code examples
reactjscomparisonreact-hooks

New React Hooks confused


I'm new with React hooks functional component, I have some questions which need to be explained, thank you:

  1. How do I can perform a deep comparison of some state before re-render? I saw that React.memo only has the 2nd argument for modify a comparison on props only, but how about the state? Currently, it uses Object.is for compare, I want to modify this function.

  2. What is the principles of useCallback, useMemo? I've investigated react source code but still dont know about how can it do this. Does someone give me a simple example to illustrate how it know to cache the value on each render?


Solution

  • Q1: How do I can perform a deep comparison of some state before re-render?

    In the snippet below, I show you one way of making a deep comparison of the state.

    Q2: What is the principles of useCallback, useMemo?

    useMemo is meant to be used in a situation where you have some expensive calculation that you want to memoize the results. So the expensive calculation only runs once for each unique combination of inputs.

    useCallback is meant to avoid the re-creation of inner functions that don't need to be recreated. Unless some variable listed in the dependency array changes. It's commonly used as a performance optimization.

    As per React DOCS:

    useCallback

    This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders (e.g. shouldComponentUpdate).

    Note:

    State changes are meant to cause a re-render. Why would you want to deep compare it before re-rendering? If it's changed, React MUST re-render. That's how it works.

    If you want something to change and not cause a re-render, take a look at the useRef hook. That's exactly what it does. The ref object remains the same across every render (only changes if the component gets unmounted, and then re-mounted).

    https://reactjs.org/docs/hooks-reference.html#useref

    useRef

    useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.

    (...)

    However, useRef() is useful for more than the ref attribute. It’s handy for keeping any mutable value around similar to how you’d use instance fields in classes.

    SNIPPET

    Comparing newState and lastState

    function App() {
    
      const [myState, setMyState] = React.useState('Initial state...');
      const lastState_Ref = React.useRef(null);
      const stateChanged_Ref = React.useRef(null);
      
      
      
      if (lastState_Ref.current !== null) {
        lastState_Ref.current === myState ?   // YOU CAN PERFORM A DEEP COMPARISON IN HERE
          stateChanged_Ref.current = false
        : stateChanged_Ref.current = true;
      }
      
      lastState_Ref.current = myState;
    
      function handleClick() {
        setMyState('New state...');
      }
    
      return(
        <React.Fragment>
          <div>myState: {myState}</div>
          <div>New state is different from last render: {JSON.stringify(stateChanged_Ref.current)}</div>
          <button onClick={handleClick}>Click</button>
        </React.Fragment>
      );
    }
    
    ReactDOM.render(<App/>, document.getElementById('root'));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
    <div id="root"/>

    UPDATE:

    From your comment, what you could do regarding props is to use React.memo. It will do a shallow compare on your props. See snippet below. You'll see that the regular Child will re-render every time and the ChildMemo, which is wrapped in React.memo won't re-render if the array propA remains the same.

    React.memo

    React.memo is a higher order component. It’s similar to React.PureComponent but for function components instead of classes.

    If your function component renders the same result given the same props, you can wrap it in a call to React.memo for a performance boost in some cases by memoizing the result. This means that React will skip rendering the component, and reuse the last rendered result.

    By default it will only shallowly compare complex objects in the props object. If you want control over the comparison, you can also provide a custom comparison function as the second argument.

    That's what React DOCs recommend for that case:

    https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-shouldcomponentupdate

    function App() {
      console.log('Rendering App...');
      
      const [myState,setMyState] = React.useState([1,2,3]);
      const [myBoolean,setMyBoolean] = React.useState(false);
      
      return(
        <React.Fragment>
          <button onClick={()=>setMyBoolean((prevState) => !prevState)}>Force Update</button>
          <Child
            propA={myState}
          />
          <ChildMemo
            propA={myState}
          />
        </React.Fragment>
      );
    }
    
    function Child() {
      console.log('Rendering Child...');
      
      return(
        <div>I am Child</div>
      );
    }
    
    const ChildMemo = React.memo(() => {
      console.log('Rendering ChildMemo...');
      return(
        <div>I am ChildMemo</div>
      );
    });
    
    ReactDOM.render(<App/>, document.getElementById('root'));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
    <div id="root"/>

    Regarding state, I don't think you should try to accomplish that. It should re-render if the state has changed. Actually React won't re-render if the state reference (of an object, or array) remains the same. So, if you don't change the state reference (like re-creating from scratch the same array or object), you already have that behavior out of the box. You can see that on the snippet below:

    function App() {
      console.log('Rendering App..');
      const [myArrayState,setMyArrayState] = React.useState([1,2,3]);
      
      function forceUpdate() {
        setMyArrayState((prevState) => {
          console.log('I am trying to set the exact same state array');
          return prevState;                     // Returning the exact same array
        });
      }
      
      return(
        <React.Fragment>
          <div>My state is: {JSON.stringify(myArrayState)}</div>
          <button onClick={forceUpdate}>Reset State(see that I wont re-render)</button>
        </React.Fragment>
      );
    }
    
    ReactDOM.render(<App/>, document.getElementById('root'));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
    <div id="root"/>