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

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

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:
Run dispatch with an object to store some data. This function is executed asynchronically, so the result is not available immediately.
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.

Related

why is my gloval setInterval ID variable unavailable to a function?

I am writing a simple timer app and using setInterval for the first time. The project is in react typescript and I use the useReducer hook to manage state.
The project requires two separate timers, session and break, which may explain some of the code. The project also requires one button to start and stop the timer, hence I am using a single function.
I have redacted my reducer and types as the problem, as i understand, does not involve them, but some simple use of setInterval that I just don't get yet.
When the startStopTimer function is called a second time, the intervalID is undefined, even though it is declared globally.
const App = () => {
const [timerState, dispatch] = useReducer(timerStateReducer, initialTimerState
);
let intervalID: NodeJS.Timer;
const startStopTimer = () => {
let name: string = timerState.session.count !== 0 ? 'session' : 'break';
if (timerState[name].running) {
console.log('stopping timer' + intervalID);
//reducer sets running boolean variable to false
dispatch({type: ActionKind.Stop, name: name});
clearInterval(intervalID);
} else {
//reducer sets running boolean variable to true
dispatch({type: ActionKind.Start, name: name});
intervalID = setInterval(() => {
dispatch({type: ActionKind.Decrease, name: name});
}, 1000);
}
};
return (
//redacted JSX code
<button onClick={startStopTimer}>Start/Stop</button>
)
I have tried passing onClick as an arrow function (rather than a reference, i think?) and it behaves the same. I tried simplifying this with useState, but came across a whole 'nother set of issues with useState and setInterval so I went back to the useReducer hook.
Thanks!

Copying an array from React Hook

I'm using the useState hook to populate items in an array.
const [meals, setMeals] = useState([]);
useEffect(async () =>{
const mealsData = await fetchMeals(localStorage.getItem('user_id'));
console.log(mealsData);// Array(3)
setMeals([...meals, mealsData]);
console.log(meals);//[]
}, []);
The array doesn't get copies in my setMeals method, what am I doing wrong here?
This is because
setMeals([...meals, mealsData]);
is an asynchronous call and you are logging the meals before being sure the state actually updated
console.log(meals);
You should call and check your console.log() function in another useEffect() call which runs on every state change and assures that the state actually changed
useEffect(() => {
console.log(meals);
}, [meals]);
[meals] passed as the second argument will let this useEffect() run every time meals (actually) update.
the update is asynchronous, the new value is not available immediately as you're expecting. You can find an in-depth explanation here: useState set method not reflecting change immediately
UseEffect cannot be async you should wrap it in an async function
function useMeals() {
const [meals, setMeals] = useState([]);
useEffect(() => {
async function setFetchedMeals() {
const mealsData = await fetchMeals(localStorage.getItem('user_id'));
setMeals([...meals, mealsData]);
}
setFetchedMeals();
}, []); // letting the dependency array empty will run the effect only once
console.log(meals)
return meals;
}
You are actually not doing anything wrong, state updates are not synchronous, so logging meals immediately after updating it will not show the updated value
Use an effect for the meals array to detect when it has actually changed...
useEffect(() => {
console.log(meals);
}, [meals]);

setState doesn't update all state properties after call

The thing is, that I use setState to update state.n and state.data. I want to call setState onClick, using handler. After calling setState, I perform a callback console.log to watch the changed state properties, but only state.data is changed, not state.n.
handler(e) {
this.setState((state) => {
let arr = state.data;
arr.push(fakeTableData[state.n+1]);
this.setState({n: state.n+1, data: arr});
}, console.log.bind(null, this.state.n));
}
After one handler used, I expect to see state.n incremented, but I had only state.data changed, not state.n. And I recieve a warning, like this: "Warning: An update (setState, replaceState, or forceUpdate) was scheduled from inside an update function. Update functions should be pure, with zero side-effects. Consider using componentDidUpdate or a callback." I've read a tutorial on the main react site, but there not enough info.
let's try let arr = [...state.data]
You should not use setState inside function that sets state. That is why you got an error. Instead you should return state. Try this:
handler = (e) => {
this.setState((state) => {
let arr = state.data;
arr.push(fakeTableData[state.n+1]);
return {n: state.n+1, data: arr};
}, () => { console.log(this.state.n) });
}

Ensuring React state has updated for game loop

I am writing a version of Conway's Game of Life in React. The component's state contains the grid describing which of the cells is alive at the current time. In each game loop, the new grid is calculated and the state is updated with the next iteration.
It occurs to me that since setState is asynchronous, when repeatedly calling the iterate function with setInterval, I am not guaranteed to be using the current version of grid each time iterate runs.
Is there an alternative to using setInterval in React that would avoid any potential issues caused by setState being asynchronous?
Here are the relevant functions that describe the game loop:
go = () => {
const { tickInterval } = this.state;
this.timerId = setInterval(this.iterate, 570 - tickInterval);
this.setState({
running: true,
});
};
iterate = () => {
const { grid, gridSize, ticks } = this.state;
const nextGrid = getNextIteration(grid, gridSize);
this.setState({
grid: nextGrid,
ticks: ticks + 1,
});
};
If you need to set state based on a current state, it is wrong to directly rely on this.state, because it may be updated asynchronously. What you need to do is to pass a function to setState instead of an object:
this.setState((state, props) => ({
// updated state
}));
And in your case it would be something like:
iterate = () => {
this.setState(state => {
const { grid, gridSize, ticks } = state;
const nextGrid = getNextIteration(grid, gridSize);
return {
grid: nextGrid,
ticks: ticks + 1
}
});
};
SetState is Asynchronous
this.setState({
running: true,
});
To make it synchronously execute a method:
this.setState({
value: true
}, function() {
this.functionCall()
})
If you have a look at the react official documentation, the setState api does take a callback in following format:
setState(updater[, callback])
Here the first argument will be your modified state object and second argument would be callback function to be executed when setState has completed execution.
As per the official docs:
setState() does not always immediately update the component. It may
batch or defer the update until later. This makes reading this.state
right after calling setState() a potential pitfall. Instead, use
componentDidUpdate or a setState callback (setState(updater,
callback)), either of which are guaranteed to fire after the update
has been applied. If you need to set the state based on the previous
state, read about the updater argument below.
You can have a look at official docs to get more information on this.

Redux async dispatch causes connected component error

When a button is clicked, then an API call is made (simulated by setTimeout) and when complete, that item is removed from the state. I would expect the component to not attempt to be rendered, but it is and fails, because the state it needs is no longer there.
The first fiddle showing the desired behaviour works because the call to dispatch is synchronous:
dispatch({
type: 'REMOVE',
id
});
The second fiddle shows the error because the call to dispatch is made asynchronously:
setTimeout(() => {
dispatch({
type: 'REMOVE',
id
});
}, 0);
I assumed the parent component would no longer try to render the child component because that item has been filtered out of the array. How do I remove the item from the state and prevent the component trying to re-render?
Yes this is an issue because by default redux doesn't support async calls like so what you are looking for it redux-thunk
https://github.com/gaearon/redux-thunk
Scroll down to motivation and it will basically have the exact same code you have posted here.
:::EDIT:::
Based off of your comment below I updated your code I think one of the main sources of the problem was connecting the Item component with map state to props.
const mapStateToProps = (state, { id }) => {
var {text} = state.items.filter((x) => x.id === id)[0];
return {
id: id,
text: text
};
};
const mapDispatchToProps = (dispatch) => {
return {
onRemove(id) {
setTimeout(() => {
dispatch({
type: 'REMOVE',
id
});
}, 0);
}
};
};
I know you probably didn't need it / or want it but I refactored the code a little bit to make it a little more clear but I might have gotten carried away and took it to far from your original example but if you want to take a look its posted here.
https://jsfiddle.net/kriscoulson/ogkyspsp/

Categories

Resources