React useState() hook does not update state when child.shouldComponentUpdate() returns false

Viewed 1585

I've started reading about React Hooks and tried to rewrite some small components to use them, but I've got a problem with setting state. Basically, when the child component <Content/> always returns false in shouldComponentUpdate, then parent's (<App/>.setShow) is called, but it does not update the state and <Content/> isn't rendered.

I've prepared a minimal working example below, where <App/> is a component based on React Hoos, and <App2/> is an equivalent class based Component. The latter one works, so apparently there is some nuance there I don't get.

const {useState} = React;

class Panel extends React.Component {
   shouldComponentUpdate() { return false; } 
   render() { return (<button onClick={this.props.onClick}>Toggle</button>) }
}

class Content extends React.Component {
  render() { return <div>Content</div> }
}

const App = (props) => {
  const [show, setShow] = useState(true);

  const toggle = () => {
    console.log("Toggling visibility!", show);
    setShow(!show);
  }

  console.log("[App] render()");
  return (<div >
    <h1>Hi at {Date.now()}</h1>
    <Panel onClick={toggle} />
    {show && <Content /> }
  </div>)
}

class App2 extends React.Component {
  state = { show: true }

  render() {
    const toggle = () => {
      console.log("[App] toggle", this.state.show)
      this.setState({ show: !this.state.show })
    }

    console.log("[App] render()");
    return (<div >
      <h1>Hi at {Date.now()}</h1>
      <Panel onClick={toggle} />
      {this.state.show && <Content />}
    </div>)
  }
}

ReactDOM.render(<div> <App/> <App2/> </div>, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>

<div id="app">
</app>

1 Answers

You need to use functional updates because you have closure on value of show inside toggle function.

The closure happens because shouldComponentUpdate always returns false, so on first render the value of this.props.onClick will have stale state of show === true, which won't change on subsequential renders.

const App = () => {
  const [show, setShow] = useState(true);

  // Closure on value of show
  // const toggle = () => {
  //   console.log("Toggling visibility!", show);
  //   setShow(!show);
  // }

  const toggle = () => {
    setShow(prevState => {
      console.log("Toggling visibility!", prevState);
      return !prevState;
    });
  };

  console.log("[App] render()");
  return (
    <div>
      <h1>Hi at {Date.now()}</h1>
      <Panel onClick={toggle} />
      {show && <Content />}
    </div>
  );
};

Edit gallant-saha-9z8eu

Related