How to avoid stale state in React functional component

Viewed 663

I am creating a game in React which also uses the Google Firestore in order to keep track of game data.

In the game, the user will hit "Start" and be connected to a Google Firebase collection, which starts listening for changes to the database. When something in the database changes, the React component state will change.

The problem I am having is that is seems that the version of state my listener's callback function uses is out of date. The listener is set in response to the player hitting the start button, and the component's state at that time seems to be cached.

The problematic component looks like this:

import React, { useEffect, useState } from "react";

function Counter() {
  const [state, setFullState] = useState({
    count: 0,
    clicks: 0,
    started: false
  });

  const setState = update => {
    setFullState(prevState => {
      return {
        ...prevState,
        ...update
      };
    });
  };

  const handleStart = () => {
    const increment = () => {
      const { clicks } = state;
      setState({ clicks: clicks + 1 });
    };
    setState({
      started: true
    });
    document.addEventListener("click", increment);
  };

  const { clicks, count, started } = state;

  if (!started) {
    return (
      <div>
        <button onClick={handleStart}>START</button>
      </div>
    );
  }

  return (
    <div>
      <h2>Hooks</h2>
      <p>Count: {count}</p>
      <p>Clicks: {clicks}</p>
      <p>
        <button
          onClick={() => {
            setState({ count: count + 1 });
          }}
        >
          More
        </button>
      </p>
    </div>
  );
}

export default Counter;

I created a sandbox to demonstrate the problem. When you click on the button, the "Clicks" number does not increase.

What is a good way to make a function reference the component's state at the time the function is called, not at the time the function is created?

1 Answers

Using useRef you can keep a snapshot of the state visible in the now stale increment callback, and keep it in sync.

const currentState = React.useRef(state)
currentState.current = state

then

  const handleStart = () => {
    const increment = () => {
      const { clicks } = currentState.current;
      setState({ clicks: clicks + 1 });
    };
    setState({
      started: true
    });
    document.addEventListener("click", increment);
  };

This should answer your question, but your problem can be solved in a better way than this.

Related