You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 12:27:47