Cannot access updated data from useReducer hook in function defined in setTimeout

1k views Asked by At

In my application, I'm using a dispatch from useReducer hook on click of a button and in the same function I'm using a setTimeout of 2 seconds. But when I store the data using dispatch of usereducer then I'm not getting updated value in setTimeout function.

I cannot share original code, but sharing a snippet of another demo app where this issue occurs.

const initialData = { data: "ABC" };

function reducer(state = initialData, action) {
  switch (action.type) {
    case "STORE":
      return {
        ...state,
        data: action.payload
      };
    default:
      return state;
      break;
  }
}
function Demo() {
  const [state, dispatch] = React.useReducer(reducer, initialData);
  console.log("Render : ",state.data);  //Will be executed on each rendering
  const handleClick = () => {
    dispatch({
      type: "STORE",
      payload: state.data + parseInt(Math.random() * 10)
    });
    setTimeout(() => {
      console.log("ButtonClick : ",state.data); //Will be executed after 2 seconds of dispatching.
    }, 2000);
  };
  return <button onClick={handleClick}>{state.data}</button>;
}
ReactDOM.render(<Demo />, document.getElementById("app"));


In above example I'm storing data in reducer using dispatch, and I'm calling console.log("ButtonClick") on Button Click after 2 seconds but even after 2 seconds, I'm not getting updated data in console. But in console.log("Render") I'm getting updated data.

Live example on : https://codepen.io/aadi-git/pen/yLJLmNa

1

There are 1 answers

7
Peter Lehnhardt On BEST ANSWER

When you call

const handleClick = () => {
  dispatch({
    type: "STORE",
    payload: state.data + parseInt(Math.random() * 10)
  });
  setTimeout(() => {
    console.log("ButtonClick : ",state.data); //Will be executed after 2 seconds of dispatching.
  }, 2000);
};

this is what happens:

  1. Run dispatch with an object to store some data. This function is executed asynchronically, so the result is not available immediately.

  2. Register a timeout handler, which logs the current value of state.data to the console. Since the preceding dispatch is still working in progress, the value of state.data is still the old one.

    This means you can not log a new dispatched value by running console.log after your dispatch call because you can not see into the future. You can only log the new data after a re-render of your component due to changing state. Then you can and should use

    React.useEffect(() => {
      console.log(state.data);
    }, [state.data]);
    

Some more explanations about setTimeout and why console.log logs old values within it

You use

setTimeout(() => {
  console.log("ButtonClick : ", state.data);
}, 2000);

and this is equivalent to

const callback = () => console.log("ButtonClick : ", state.data);
setTimeout(callback, 2000);

In the first line you create a function (named callback here), which prints some text. This text consists of a simple string and the value of state.data. Therefore this function has a reference to the variable state.data. The function in combination with the reference to the outer state is called closure and this closure ensures, that the value state.data is kept alive as long as the function lives (is not binned by the garbage collector).

In the second line setTimeout is called with this function. Simplified this means a task is stored somewhere which states, that exactly this function has to be executed after the given timeout. So as long as this task is not done, the function stays alive and with it the variable state.data.

In the meantime long before the task is handled, the new action is dispatched, new state calculated and Demo re-rendered. With that a new state.data and a new handleClick is created. With the new creation of handleClick also a new function is created which is passed to setTimeout. This whole handleClick function is then passed as onClick handler for the button element.

The re-render is now over, but the task from before is still pending. Now when the timeout duration ends (long after re-rendering the component) the task is taken from the task queue and executed. The task still has reference to the function from the render before and this function still has reference to the value state.data from the render before. So the value logged to the console is still the old value from the render before.