useState getting reset to default when used in combination with useRef in strict mode

Viewed 36

I'm seeing a piece of useState state reset itself to default. This is only happening in strict mode (which I'm using because it's on by default in my Next.js app in development mode, and that's where I came across this issue).

Here's a running example in Codepen. It has React 18 enabled in development mode. https://codepen.io/tremby/pen/LYdwWXK?editors=1111

Here's a copy of the code:

const { useEffect, useState, useRef } = React;

function useFakeQuery() {
  const [hookState, setHookState] = useState(null);
  useEffect(() => {
    console.log("starting timeout");
    const timeout = setTimeout(() => {
      console.log("timeout completed, returning response");
      setHookState("response");
    }, 1000);
    return function cleanup() {
      console.log("cancelling timeout, no response");
      clearTimeout(timeout);
    }
  }, [setHookState]);
  return hookState;
}

const Example = () => {
  useEffect(() => {
    console.log("mount");
    return function cleanup() {
      console.log("unmount");
    }
  }, []);

  const [myState, setMyState] = useState("default");
  const queryResponse = useFakeQuery();

  console.log("state is", myState, ", query response is", queryResponse);

  const loadedRef = useRef(false);
  if (!loadedRef.current && queryResponse != null) {
    console.log("query has responded (for the first time) with", queryResponse);
    loadedRef.current = true;
    if (myState === "default") {
      console.log("state was still at default, so setting state to query response:", queryResponse);
      setMyState(queryResponse);
    }
  }

  return <p>State: {myState}</p>;
}


const App = () => {
  return(
    <React.StrictMode>
      <Example />
    </React.StrictMode>
  );
}

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

You'll see in the console that it mounts and unmounts, then mounts again. If I understand correctly, that's a part of strict mode, and is expected behaviour.

You'll then see that the timeout from that first mount is cancelled, correctly, and the second one completes. The Example component picks that up, and so sets its loadedRef to true, and sets its myState to the response ("response") via setMyState.

There's another render cycle, which logs state is response , query response is response which is how I expect things to end up -- and the rendered output should be "State: response".

But then there's another final render cycle, which logs state is default , query response is response. The state has somehow been reset to its default value, and the rendered output is "State: default".

Why is this? What is going on?

Is this a React bug, or am I doing something wrong?


  • If I use another piece of state rather than a ref to record whether a response has come back from the hook, the problem disappears, but I'm under the impression that a ref is a perfectly reasonable pattern to follow here. Tell me why I'm wrong, if so, please.
  • If I switch off strict mode, the problem disappears. But I don't want to do that.
0 Answers
Related