I use useState
in my custom React hook useGame
and I was pretty sure that updating state in the custom hook would trigger re-rendering of every component using the hook but it turned out it doesn't work this way
useGame hook
export const useGame = () => {
const [stage, setStage] = useState(STAGE.START);
const [minValue, setMinValue] = useState(DEFAULT_MIN);
const [maxValue, setMaxValue] = useState(DEFAULT_MAX);
const [number, setNumber] = useState(generateRandomNumberInRange(minValue, maxValue));
const generateNumber = () => {
const newNumber = generateRandomNumberInRange(minValue, maxValue);
setNumber(newNumber);
};
return { stage, setStage, number, generateNumber };
};
Game component
export const Game = () => {
const { stage } = useGame();
return (
<div style={{ display: "flex", flexDirection: "column", justifyContent: "center", alignItems: "center" }}>
{(() => {
switch (stage) {
case STAGE.START:
return <Start />;
case STAGE.PLAYING:
return <Playing />;
case STAGE.FINISH:
return <Finish />;
default:
return null;
}
})()}
</div>
);
};
Start component
export const Start = () => {
const { setStage } = useGame();
const handleClick = () => {
setStage(STAGE.PLAYING);
};
return (
<>
<button onClick={handleClick}>Start game</button>
</>
);
};
I was sure that on handleClick
it would update state stage
in useGame
hook, Game
component would re-render and since the stage
had changed it would render Playing
component this time. But it doesn't work this way. Could you please explain me what I am doing wrong and how to fix my code and make it work?
CodePudding user response:
The useGame()
calls in <Game />
and <Start />
aren't related — they will both have their own states. React doesn't exactly know that your useGame
hook exists; it only sees that while rendering the <Game />
component (i.e. calling the Game
function), useState
gets called four times, and while rendering the <Start />
component (again, that is calling the Start
function), useState
gets called four times too. React can keep track of the eight state values by knowing where the <Game />
and <Start />
components exist in its tree model and combining that information with the order of the useState
calls.
Game()
useGame()
(React doesn't see this call)useState(STAGE.START)
(React sees this) — State #0useState(DEFAULT_MIN)
(React sees this) — State #1useState(DEFAULT_MAX)
(React sees this) — State #2useState(generate…)
(React sees this) — State #3
<Start />
useGame()
(React doesn't see this call)useState(STAGE.START)
(React sees this) — State #4useState(DEFAULT_MIN)
(React sees this) — State #5useState(DEFAULT_MAX)
(React sees this) — State #6useState(generate…)
(React sees this) — State #7
To solve this, you'll either need to move the state up (to a common ancestor component for <Game />
and <Start />
which will then pass the states and setters down as props or as a context value) or use an external state manager like Zustand, Redux, MobX etc.