Search code examples
javascriptreactjsreact-hooksreactive-programming

shouldComponentUpdate equivalent for functional component, to ignore state changes


My code has a component that takes both props and has its own internal state.
The component should rerender ONLY when its props change. State changes should NOT trigger a rerender.
This behaviour can be implemented with a class based component and a custom shouldComponentUpdate function.
However, this would be the first class based component in the codebase. Everything is done with functional components and hooks. Therefore I would like to know whether it is possible to code the desired functionality with functional components.

After a few answers that didn't approach the real problem, I think I have to reformulate my question. Here is a minimal example with two components:

  • Inner takes a prop and has state. This is the component in question. It must not rerender after state changes. Prop changes should trigger a rerender.
  • Outer is a wrapper around inner. It has no meaning in the scope of this question and is only there to give props to Inner and to simulate prop changes.

To demonstrate the desired functionality I have implemented Inner with a class based component. A live version of this code can be found on codesandbox. How can I migrate it to a functional component:

Inner.tsx:

import React, { Component } from 'react'

interface InnerProps{outerNum:number}
interface InnerState{innerNum:number}

export default class Inner extends Component<InnerProps, InnerState> {
    state = {innerNum:0};

    shouldComponentUpdate(nextProps:InnerProps, nextState:InnerState){
        return this.props != nextProps;
    }
    render() {
        return (
            <button onClick={()=>{
                this.setState({innerNum: Math.floor(Math.random()*10)})
            }}>
                {`${this.props.outerNum}, ${this.state.innerNum}`}
            </button>
        )
    }
}

Outer.tsx:

import React, { useState } from "react";
import Inner from "./Inner";

export default function Outer() {
  const [outerState, setOuterState] = useState(1);

  return (
    <>
      <button
        onClick={() => {
          setOuterState(Math.floor(Math.random() * 10));
        }}
      >
        change outer state
      </button>
      <Inner outerNum={outerState}></Inner>
    </>
  );
}

The official docs say to wrap the component in React.memo. But this doesn't seem to work for preventing rerenders on state change. It only applies to prop changes.

I have tried to make React.memo work. You can see a version of the code with both Outer and Inner being functional components here.

Related questions:

How to use shouldComponentUpdate with React Hooks? : This question only deals with prop changes. The accepted answer advises to use React.memo

shouldComponentUpdate in function components : This question predates stateful functional components. The accepted answer explains how functional components don't need shouldComponentUpdate since they are stateless.


Solution

  • React is by design driven by setState -> re-render loop. Props change is in fact a setState somewhere in parent components. If you don't want the setState to trigger a re-render, then why in the first place use it?

    You can pull in a const state = useRef({}).current to store your internal state instead.

    function InnerFunc(props) {
      const state = useRef({ innerNum: 0 }).current;
      return (
        <button
          onClick={() => {
            state.innerNum = Math.floor(Math.random() * 10);
          }}
        >
          {`${props.outerNum}, ${state.innerNum}`}
        </button>
      );
    }
    

    That said, it's still a valid question to ask: "how to implement shouldComponentUpdate in a react hook fashion?" Here's the solution:

    function shouldComponentUpdate(elements, predicate, deps) {
      const store = useRef({ deps: [], elements }).current
      const shouldUpdate = predicate(store.deps)
      if (shouldUpdate) {
        store.elements = elements
      }
      store.deps = deps
      return store.elements
    }
    
    // Usage:
    
    function InnerFunc(props) {
      const [state, setState] = useState({ innerNum: 0 })
      const elements = (
        <button
          onClick={() => {
            setState({ innerNum: Math.floor(Math.random() * 10) });
          }}
        >
          {`${props.outerNum}, ${state.innerNum}`}
        </button>
      );
    
      return shouldComponentUpdate(elements, (prevDeps) => {
        return prevDeps[0] !== props
      }, [props, state])
    }
    

    Noted that it's impossible to prevent a re-render cycle when setState is called, the above hook merely makes sure the re-rendered result stays the same as prev rendered result.