React-Query结合useEffect引发无限循环问题求助
问题根源分析
queryResponses引用不稳定:每次组件渲染时,userIds.map(...)都会生成全新的数组对象,导致useEffect的依赖[queryResponses]每次都被判定为“变化”,触发effect执行。- state更新触发重渲染循环:effect中调用
setUsers/setErrors/setIsLoading会触发组件重渲染,重渲染又会生成新的queryResponses,再次触发effect,形成无限循环。 - React-Query返回对象的引用变化:每个
useGetUser返回的查询状态对象(包含isLoading/isSuccess等),在状态切换时会生成新的引用,这也会导致queryResponses数组被视为新依赖,进一步触发effect。
解决方案:使用React-Query官方的
useQueries钩子 React-Query提供了useQueries专门处理多个并行查询的场景,无需手动维护本地state,从根源避免循环:
import { useQueries } from '@tanstack/react-query'; // 假设你的useGetUser内部是基于fetchUser函数实现的API调用 const fetchUser = async (userId: number): Promise<IUser> => { const res = await fetch(`/api/users/${userId}`); if (!res.ok) throw new Error('Failed to fetch user'); return res.json(); }; const YourComponent = ({ userIds }: { userIds: number[] }) => { // 用useQueries批量处理所有用户查询 const queryResults = useQueries({ queries: userIds.map(userId => ({ queryKey: ['user', userId], // 每个查询的唯一标识key queryFn: () => fetchUser(userId), })) }); // 直接从查询结果计算所需状态,无需本地state const isLoading = queryResults.some(query => query.isLoading); const errors = queryResults .filter(query => query.isError) .map(query => query.error as IException); const users = queryResults .filter(query => query.isSuccess) .map(query => query.data as IUser); // 渲染逻辑 if (isLoading) return <div>加载中...</div>; if (errors.length > 0) { return <div>加载出错:{errors.map(e => e.message).join('、')}</div>; } return ( <div> {users.map(user => ( <div key={user.id}>{user.name}</div> ))} </div> ); };
为什么这个方案能解决问题
useQueries返回的结果数组会稳定更新,只有当查询状态实际变化时才会更新引用,不会每次渲染都生成新数组。- 直接在渲染阶段计算
isLoading/users/errors,无需通过useEffect更新本地state,彻底避免了state更新触发的重渲染循环。 - 遵循React-Query的设计理念:由库负责管理查询状态,组件只需要基于查询结果做渲染逻辑。
极端场景下的临时方案(不推荐)
若因特殊需求必须维护本地state,需修改useEffect的依赖项,避免依赖整个queryResponses数组,改为依赖每个查询的具体状态值:
// 提取所有查询的关键状态作为依赖 const dependencies = queryResponses.flatMap(response => [ response.isLoading, response.isSuccess, response.isError, response.data, response.error ]); useEffect(() => { const fetchedUsers: IUser[] = []; const apiErrors: IException[] = []; let isApiLoading = false; queryResponses.forEach((response) => { if (response.isLoading) { isApiLoading = true; } if (response.isSuccess && response.data) { fetchedUsers.push(response.data); } if (response.isError && response.error) { apiErrors.push(response.error); } }); // 只有当值变化时才更新state,避免不必要的重渲染 if (JSON.stringify(fetchedUsers) !== JSON.stringify(users)) { setUsers(fetchedUsers); } if (JSON.stringify(apiErrors) !== JSON.stringify(errors)) { setErros(apiErrors); } if (isApiLoading !== isLoading) { setIsLoading(isApiLoading); } }, dependencies); // 使用提取的具体状态作为依赖
注意:这种方式比较繁琐,且
JSON.stringify的对比存在性能隐患,仅作为极端场景下的临时方案,优先推荐使用useQueries。
内容的提问来源于stack exchange,提问作者Ranjan Kumar
相关产品推荐
相关产品推荐

