Angular转React:如何避免无限循环,正确使用useState与自定义Hook
问题描述
从Angular转向React搭建Demo时,遇到无限循环及副作用问题:
- 容器组件
GamePage.tsx从首页跳转进入,加载时通过封装TanStack Query的自定义Hook从API获取数据 - 尝试将返回数据存入组件状态并传递给子组件展示,但设置状态时触发无限循环
- 当前容器组件已很精简,疑惑是否需要新增子组件来调用自定义Hook,寻求符合React范式的解决方案
相关代码
自定义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> ) };
解决方案
核心问题是误解了TanStack Query的作用——它已经帮你管理了数据请求、加载/错误状态,不需要再用useState二次存储数据,这正是无限循环的根源。同时自定义Hook的queryKey配置错误,导致重复请求。
1. 修复自定义Hook的queryKey
queryKey是TanStack Query识别请求唯一性的标志,必须包含依赖项(这里是difficulty),否则每次组件渲染都会重新发起请求:
const useWordByDifficulty = ({ difficulty }: { difficulty: Difficulty }) => { const wishedLength = mapDifficultyToLength(difficulty); return useQuery({ // 将difficulty加入queryKey,难度变化时自动重新请求 queryKey: ['game-word', difficulty], queryFn: () => httpFetch(`API_URL`) }); };
2. 简化GamePage组件,直接使用TanStack Query的状态
Hook必须在组件顶层调用,直接解构返回的data、isPending、isError状态即可,完全不需要额外的useState或useEffect:
const GamePage = () => { const { state }: { state: { difficulty: Difficulty } } = useLocation(); // 顶层调用自定义Hook,解构状态 const { data: gameWord, isPending, isError } = useWordByDifficulty(state); return ( <div> <h1>Game Page</h1> <h3>Diff: {state.difficulty}</h3> {isPending && <div>Loading...</div>} {isError && <div>An error occurred...</div>} {gameWord && <GameWord gameWord={gameWord} difficulty={state.difficulty} />} </div> ) };
3. 为什么不需要新增子组件?
当前组件逻辑已经足够精简,完全符合React范式——在组件顶层调用Hook获取状态,直接用于渲染。只有当组件逻辑过于复杂时,才需要拆分子组件解耦,但目前的场景完全不需要。
关键说明
- 无限循环的根源:试图用
setGameWord把Query的data存到useState,每次调用setGameWord都会触发组件重渲染,重渲染又会调用Hook获取数据,再次setGameWord,形成循环。 - TanStack Query的价值:它内置了缓存、自动重新请求、状态管理,不需要自己手动维护请求状态和数据存储。
内容的提问来源于stack exchange,提问作者Francesco
相关产品推荐
相关产品推荐

