Reputation: 5148
I create this simple example script to animate a player position using a simple setInterval function but impossible to make it works..
Everytime it go into the interval function the playerPos
is back to is initial value..
import React, { useEffect, useState } from 'react';
const App = () => {
let [playerPos, setPlayerPos] = useState({x:128, y:128})
useEffect(() => {
setInterval(() => {
console.log(`Current player pos ${JSON.stringify(playerPos)}`);
setPlayerPos({x:128, y:playerPos.y + 10});
}, 1000);
}, []);
return <div style={{position: "absolute", top: `${playerPos.x}px`, left: `${playerPos.y}px`}}></div>
}
Strangely, my console only showing:
Current player pos {"x":128,"y":128}
Current player pos {"x":128,"y":128}
Current player pos {"x":128,"y":128}
...
What did I misunderstood in hooks and effects ??!
Upvotes: 4
Views: 2920
Reputation: 218818
The useEffect
only ran once, when the component initially loaded, and at the time only had the initial value of the playerPos
state. So you effectively have a closure around that one value that's just repeating.
Instead of using setInterval
, rely on useEffect
to run multiple times whenever the state changes by adding that state value to its dependency array. Then just use setTimeout
to add the delay each time. For example:
useEffect(() => {
setTimeout(() => {
console.log(`Current player pos ${JSON.stringify(playerPos)}`);
setPlayerPos({x:128, y:playerPos.y + 10});
}, 1000);
}, [playerPos]);
So each time the component renders, useEffect
will re-run if playerPos
has changed. And each time it runs, playerPos
will be changed one second later, triggering a re-render.
Note that this would always leave an active timeout when the component unloads. So as an additional step you'll want to provide useEffect
with a way to cancel the timeout. How one does this is by returning a "cleanup" function form the useEffect
operation, which would release any resources held by the logic within the operation.
In this case it's just a simple call to clearTimeout
:
useEffect(() => {
const timeout = setTimeout(() => {
console.log(`Current player pos ${JSON.stringify(playerPos)}`);
setPlayerPos({x:128, y:playerPos.y + 10});
}, 1000);
return () => clearTimeout(timeout);
}, [playerPos]);
Upvotes: 5
Reputation: 84912
After the first render, you set up the interval, and the code closes over the playerPos that existed at that time. That's an object with {x: 128, y: 128}
, and it always will be. The empty dependency array means the effect will never run again, and setting state does not (and can not) replace the local variable playerPos
.
Since all you're doing is setting state, you can fix this by using the function version of set state. React will pass you the most recent value of the state, and then you can calculate your new state based on that.
useEffect(() => {
const id = setInterval(() => {
setPlayerPos(prev => {
console.log("Current player pos", JSON.stringify(prev));
return { x: 128, y: prev.y + 10 }
});
}, 1000);
return () => {
clearInterval(id);
}
}, []);
Your other option is to change the way you do the effect so that you set up a new timeout every time the position changes. That way, each new timeout closes over the most recent version of the state.
useEffect(() => {
const id = setTimeout(() => {
console.log("Current player pos", JSON.stringify(playerPos));
setPlayerPos({ x: 128, y: playerPos.y + 10 });
}, 1000);
return () => {
clearTimeout(id);
}
}, [playerPos]
Upvotes: 2