Coming from Angular I am having difficulty structuring a simple demo with React as I run into infinite loops and other side effects.
I have a GamePage.tsx
(container component) that the user navigates from the home page. When this component loads, I fetch some data from an API with Tanstack Query (code wrapped in a custom hook).
According to the result, I store the result in the component's state prop and display a child component with it.
However, while I think storing the returned data from the API seems a good candidate to be held in the components' state, by setting it would trigger an infinite loop. Should I have to create another child component in the GamePage.tsx just to use/call the custom hook? This seems a bit too verbose and not really bringing benefits as the container component is already pretty minimal. Considering the case below (Container component -> Fetch Data -> init child component) what should be changed in my solution to follow the "React way"? From the docs I could not find a unique/common answer for this.
The custom Hook
const useWordByDifficulty = ({ difficulty }: { difficulty: Difficulty }) => {
const wishedLength = mapDifficultyToLength(difficulty);
return useQuery({
queryKey: [],
queryFn: () => httpFetch(`API_URL`)
});
};
GamePage.tsx
const GamePage = () => {
const { state }: { state: { difficulty: Difficulty } } = useLocation();
// While I think the state here might be a good choice, it triggers an infinite loop setting it in the code below
//const [gameWord, setGameWord] = useState<string | null>(null);
let reqStatus: HttpResponseStatus | null = null;
let gameWord: string | null = null;
// useEffect(() => { <-- This is not permitted as useWordByDifficulty is a custom hook
let active = true;
// The API is called twice
const { data, error, isPending, isError } = useWordByDifficulty(state)
if (active) {
if (isPending) {
reqStatus = 'isPending';
} else if (isError) {
reqStatus = 'isError';
} else {
// If I set the word here, I get an infinite loop as the component re-render
// setGameWord(data);
reqStatus = 'isSuccess';
}
}
return () => {
// cleanup
active = false;
}
// }, [state]);
return (
<div>
<h1>Game Page</h1>
<h3>Diff: {state.difficulty}</h3>
{reqStatus === 'isPending' && <div>Loading...</div>}
{reqStatus === 'isError' && <div>An error occurred...</div>}
{reqStatus === 'isSuccess' && <GameWord gameWord={gameWord} difficulty={state.difficulty}></GameWord>}
</div>
)
};
Keep your custom hooks out of the useEffect
(see also Rules of Hooks) and call them from the root of your component. You can then call the resulting data from inside your useEffect
and add the data to the dependency array of the useEffect
. If your only intention is to write the data to a variable then you can skip the useEffect
and useState
.
The API is likely triggered twice due to you debugging with <StrictMode>
(used to check for missing Effect cleanups), in which case it works as intended. Otherwise it could be caused by a bad implementation of a custom hook (running with re-renders), but since you only return results of a widely used and tested hook that shouldn't be the case.