React+Apollo/GraphQL多useQuery加载时显示加载态的问题
我正在学习GraphQL/Apollo与React,需要使用useQuery或useLazyQuery获取数据。具体来说,我有两个查询,查询B依赖查询A的结果,需根据A的结果决定是否跳过B,同时要显示加载spinner。当前我采用useEffect(依赖为空数组)+useLazyQuery的方案实现一次性加载数据并更新加载状态,代码如下:
export const QUERY_A = gql ` // something ` export const QUERY_B = gql ` // something ` export default function ExampleModal(props: SomeType) { const [apiStatus, setApiStatus] = React.useState({ loading: false, error: false }); let resultA = ''; const[getA] = useLazyQuery(QUERY_A, { onCompleted: (data) => { // won't set loading to false, as we need to queryB resultA = data?.getAquery.status }, onError: (error) => { setApiStatus({ loading: false, error: true }); }, }); let resultB = ''; const[getB] = useLazyQuery(QUERY_B, { onCompleted: (data) => { resultB = data?.getBquery.result; setApiStatus({ loading: false, error: false, }); }, onError: (error) => { setApiStatus({ loading: false, error: true }); }, }, skip: resultA == 'something' // this situation,we skip B ); React.useEffect( () => { setApiStatus({ // start loading loading: true, error: false }); // fetch real data from A and B, }, []); // use [] to just load for one time, when re-render, will not call apis again if (apiStatus.loading) return <div> loading...</div> if (apiStatus.error) return <div> error...</div> return <div> real result </div> }
疑问
- 在useEffect中调用setApiStatus设置loading为true后,组件是否会立即重渲染?
- 如何存储resultA和resultB,避免重渲染后重新获取数据?将它们存入useState的apiStatus对象是否可行?
- 对于这类依赖查询的场景,有什么最佳实践建议?
回答
1. 关于setApiStatus后的重渲染
是的,调用setApiStatus更新state后,组件会立即触发重渲染。React的state更新机制就是如此——只要state发生变化,组件就会重新执行渲染逻辑。不过需要注意,useEffect本身是在组件挂载后执行的,所以第一次挂载时,先执行初始渲染,再执行useEffect里的setApiStatus,这时会触发第二次渲染,显示loading状态。
2. 存储resultA和resultB的正确方式
当前代码里用let resultA = ''和let resultB = ''是完全错误的——每次组件重渲染时,这些变量都会被重新初始化,之前存储的数据会丢失。
可行的解决方案有两种:
- 存入useState:可以把resultA、resultB单独用useState存储,或者合并到你现有的apiStatus对象里。比如:
在onCompleted回调里直接更新这个state:const [apiStatus, setApiStatus] = React.useState({ loading: false, error: false, resultA: '', resultB: '' });
这种方式完全可行,因为useState存储的数据会在组件重渲染时保留,不会丢失。onCompleted: (data) => { setApiStatus(prev => ({...prev, resultA: data?.getAquery.status})); // 之后再触发QUERY_B的查询 } - 使用useRef:如果不需要resultA/resultB触发组件重渲染,可以用useRef存储,比如
const resultARef = useRef(''),然后在onCompleted里赋值resultARef.current = data?.getAquery.status。但如果你的UI需要根据resultA/resultB更新,这种方式就不适用,因为useRef的变化不会触发重渲染。
3. 依赖查询场景的最佳实践
针对这种查询B依赖查询A结果的场景,推荐以下几种方案:
方案一:使用useQuery的skip属性串联查询
用useQuery代替useLazyQuery,利用skip属性控制查询的执行时机:
export default function ExampleModal(props: SomeType) { // 先执行QUERY_A,没有skip const { data: dataA, loading: loadingA, error: errorA } = useQuery(QUERY_A); // 根据QUERY_A的结果决定是否执行QUERY_B const skipB = dataA?.getAquery.status === 'something'; const { data: dataB, loading: loadingB, error: errorB } = useQuery(QUERY_B, { skip: skipB }); // 合并加载状态:只要A或B在加载,就显示loading const loading = loadingA || loadingB; // 合并错误状态:A或B出错都显示错误 const error = errorA || errorB; if (loading) return <div>loading...</div>; if (error) return <div>error...</div>; // 使用dataA和dataB渲染内容 return <div>real result</div>; }
这种方式不需要手动管理loading状态,Apollo会自动返回每个查询的loading和error状态,逻辑更简洁,也符合Apollo的最佳实践。
方案二:在QUERY_A的onCompleted里手动触发QUERY_B
如果必须用useLazyQuery,可以在QUERY_A的onCompleted回调里调用getB,同时用useState存储resultA:
export default function ExampleModal(props: SomeType) { const [apiStatus, setApiStatus] = React.useState({ loading: false, error: false, resultA: '', resultB: '' }); const[getA] = useLazyQuery(QUERY_A, { onCompleted: (data) => { const status = data?.getAquery.status; setApiStatus(prev => ({...prev, resultA: status})); // 根据status决定是否调用getB if (status !== 'something') { getB(); } else { // 不需要调用B,直接结束加载 setApiStatus(prev => ({...prev, loading: false})); } }, onError: (error) => { setApiStatus({ loading: false, error: true, resultA: '', resultB: '' }); }, }); const[getB] = useLazyQuery(QUERY_B, { onCompleted: (data) => { setApiStatus(prev => ({ ...prev, resultB: data?.getBquery.result, loading: false, error: false })); }, onError: (error) => { setApiStatus({ loading: false, error: true, resultA: apiStatus.resultA, resultB: '' }); }, }); React.useEffect( () => { setApiStatus(prev => ({...prev, loading: true, error: false})); getA(); // 先触发A的查询 }, []); if (apiStatus.loading) return <div>loading...</div>; if (apiStatus.error) return <div>error...</div>; return <div>real result</div>; }
这种方式需要手动管理加载和错误状态,但能更精细地控制查询触发时机。
额外建议
- 尽量避免手动管理loading状态,优先使用Apollo提供的
loading属性,减少手动维护的bug。 - 如果两个查询可以合并成一个GraphQL查询(后端支持的话),这是最优解——减少网络请求次数,性能更好。
内容的提问来源于stack exchange,提问作者Hongli Bu

