My goal is to set up a game loop but a simple test isn't working as expected. In the following component, I am trying the useEffect
hook to increment food
. I expect to see "Food: 1". Instead I see "Food: 0". When I inspect the component with the dev tools, I can see that food is 2. I've discovered that the component mounts, increments food, unmounts, mounts again and increments food once more.
I have two questions:
- Can I do something about the double mount? (like prevent it or wait until the final mount with a nested component perhaps?)
- Why does the displayed food count still equal zero? Is it because
game
inside<span>Food: {game.food}</span>
still refers to the initial instance? If so, how do I get the latest instance?
Component:
import React from "react";
class Game {
food = 0;
}
export default function App() {
const [game, setGame] = React.useState(new Game());
React.useEffect(() => {
setGame((game) => {
game.food = 1;
return game;
});
});
return <span>Food: {game.food}</span>;
}
CodePudding user response:
Don't Mutate State Objects
React uses reference comparisons and expects the reference of the root state object to change if any data within it has changed.
For Example:
// DON'T
setGame((game) => {
// mutate and return same object
game.food = 1;
return game;
});
// DO
setGame((current) => {
// create new object with updated food value
return {
...current,
food: current.food 1
};
});
Using the same reference will cause components to not update as expected.
useEffect Dependency Array
A useEffect
without a dependency array will trigger every time the component renders.
If you wish for the useEffect
to only trigger on mount provide an empty dependency array.
For Example:
// Every Render
useEffect(() => {
alert('I trigger every render');
});
// On Mount
useEffect(() => {
alert('I trigger on mount');
}, []);
// Everytime the reference for game changes
useEffect(() => {
alert('I trigger everytime the game state is update');
}, [game]);
CodePudding user response:
Conclusion
- "Mount twice" probably you are using react 18 and have strict mode enabled. It will trigger useEffect twice in dev mode from docs
- If you want to update the view, you should make the reference of the game variable changes (instead of changing its attrs).
Solution
const initialGame = {
food: 0
}
export default function App() {
const [game, setGame] = React.useState(initialGame);
React.useEffect(() => {
setGame((game) => {
game.food = 1;
return {...game};
});
}, []);
return <span>Food: {game.food}</span>;
}
CodePudding user response:
- No you should not useEffect as a loop, its execution depends on your component states and its parent component, so this leaves 3 solutions 1st
while
loop, 2ndrequestAnimationFrame
and 3rdsetInterval
. while loop is discouraged because it will block event loop and canceling/stopping can be tedious. - double mount ? i think its react double checking function, which does this only dev mode. Once you switch to
requestAnimationFrame
you won't be having that issue. - use tried mutate state and react doesn't recognizes this so it doesn't re render. solution: return new object.
updating states
useEffect(() => {
setGame((current) => {
const newState = { ...current, food: current.food 1 }
return newState
})
}, [])
using setInterval
to act as loop
useEffect(() => {
const id = setInterval(() => setCount((count) => count 1), 1000)
return () => clearInterval(id)
}, [])
using requestAnimationFrame
to act as loop
// credit: https://css-tricks.com/using-requestanimationframe-with-react-hooks/
const requestRef = React.useRef()
const animate = (time) => {
setCount((count) => count 1)
requestRef.current = requestAnimationFrame(animate)
}
useEffect(() => {
requestRef.current = requestAnimationFrame(animate)
return () => cancelAnimationFrame(requestRef.current)
}, []) // Make sure the effect runs only once