条件调用Apollo useQuery首次请求返回异常结果排查
问题根因
首次触发拿不到全量数据是三个逻辑问题叠加导致的:
- 异步时序问题:
getAll里调用的setAll是异步方法,点击触发时只会把all状态改成true,当前同步执行的代码里拿到的items还是旧的当月数据,同时useQuery切换到全量查询后需要走网络请求,不可能立刻返回结果。 - 数据判断逻辑漏洞:
useEffect里没有根据当前的all状态匹配对应的返回字段,两个if是并行执行的。Apollo Client切换查询时不会自动清空result.data上的旧字段,全量请求第一次返回时,result.data会同时存在旧的当月数据字段getCurrentMonth和新的全量字段getItems,很容易出现旧数据先入为主覆盖新值的情况,再加上React严格模式下useEffect会双调用,这个问题会更明显。 - 缺少完成态通知:
getAll方法只做了状态切换,没有等全量数据请求、写入store完成就返回,调用方如果在调用getAll后立刻读取items,拿到的必然是旧值。
另外代码里还有两个隐性问题:一是useReducer初始值传了空数组,但reducer返回的是带items属性的对象,首次渲染时state.items是undefined;二是组件顶层定义的items局部变量从来没被用到,属于完全无效的冗余代码。
修复方案
1. 修正基础逻辑错误
补全缺失的ItemContext创建、dayjs导入、useItem自定义Hook实现,把useReducer的初始值改成和reducer返回结构一致的{ items: [] },删掉没用的局部items赋值代码。
2. 优化useEffect更新逻辑
把all加入useEffect依赖数组,用互斥判断根据当前查询模式读取对应字段,避免旧字段干扰,同时给useQuery加fetchPolicy: 'network-only'配置,切换查询时不读取旧缓存。
3. 改造getAll为异步方法
让getAll在触发全量查询后,等数据完全写入store再返回,调用方可以await等待数据准备完成后再执行导出操作。
修复后的核心代码如下:
ItemsContext 代码
import React, { useState, useReducer, createContext, useContext, useEffect, useCallback, } from 'react' import { useQuery } from '@apollo/client' import dayjs from 'dayjs' import { CURRENT_MONTH, ALL_ITEMS } from '../graphql/queries' import itemReducer from '../reducers/itemReducer' const ItemContext = createContext() export const ItemProvider = ({ children }) => { const [all, setAll] = useState(false) const selectedMonth = dayjs(new Date()).format('YYYY-MM') // 修正reducer初始值结构 const [state, dispatch] = useReducer(itemReducer, { items: [] }) const result = useQuery(all ? ALL_ITEMS : CURRENT_MONTH, { variables: all ? null : { selectedMonth }, fetchPolicy: 'network-only' }) useEffect(() => { // 全量模式只读取getItems字段 if (all && result.data?.getItems) { dispatch({ type: 'ALL', items: result.data.getItems, }) return } // 当月模式只读取getCurrentMonth字段 if (!all && result.data?.getCurrentMonth) { dispatch({ type: 'ALL', items: result.data.getCurrentMonth, }) } }, [result.data, all]) const getAll = useCallback(async () => { if (all) return setAll(true) // 等待全量数据返回并写入store await result.refetch() // 等待dispatch生效 await new Promise(resolve => setTimeout(resolve, 0)) }, [all, result]) const value = { items: state.items, getAll, loading: result.loading } return <ItemContext.Provider value={value}>{children}</ItemContext.Provider> } export default function useItem() { return useContext(ItemContext) }
ExportData 组件代码
import useItem from '../../context/ItemsContext' const ExportData = () => { const { items, getAll } = useItem() const dialogConfirmation = async confirm => { if (confirm) { await getAll() // 等待getAll执行完成后再执行导出逻辑,此时items已是全量数据 // 编写对应导出逻辑即可 } } return <button onClick={() => dialogConfirmation(true)}>导出全量数据</button> }
改完之后首次点击就可以正常拿到全量数据,不需要点第二次。
内容的提问来源于stack exchange,提问作者smoothlikebutter
相关产品推荐
相关产品推荐

