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

React+Apollo/GraphQL多useQuery加载时显示加载态的问题

React+GraphQL/Apollo依赖查询问题

我正在学习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>
} 

疑问

  1. 在useEffect中调用setApiStatus设置loading为true后,组件是否会立即重渲染?
  2. 如何存储resultA和resultB,避免重渲染后重新获取数据?将它们存入useState的apiStatus对象是否可行?
  3. 对于这类依赖查询的场景,有什么最佳实践建议?

回答

1. 关于setApiStatus后的重渲染

是的,调用setApiStatus更新state后,组件会立即触发重渲染。React的state更新机制就是如此——只要state发生变化,组件就会重新执行渲染逻辑。不过需要注意,useEffect本身是在组件挂载后执行的,所以第一次挂载时,先执行初始渲染,再执行useEffect里的setApiStatus,这时会触发第二次渲染,显示loading状态。

2. 存储resultA和resultB的正确方式

当前代码里用let resultA = ''和let resultB = ''是完全错误的——每次组件重渲染时,这些变量都会被重新初始化,之前存储的数据会丢失。

可行的解决方案有两种:

  • 存入useState:可以把resultA、resultB单独用useState存储,或者合并到你现有的apiStatus对象里。比如:
    const [apiStatus, setApiStatus] = React.useState({
       loading: false,
       error: false,
       resultA: '',
       resultB: ''
    });
    
    在onCompleted回调里直接更新这个state:
    onCompleted: (data) => {
      setApiStatus(prev => ({...prev, resultA: data?.getAquery.status}));
      // 之后再触发QUERY_B的查询
    }
    
    这种方式完全可行,因为useState存储的数据会在组件重渲染时保留,不会丢失。
  • 使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 11:30:58