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

React自定义数据获取Hook:如何解决loading与error状态不一致问题

解决React自定义Hook手动模式下loading状态间隙问题

问题核心

手动模式下,Hook初始loading为false,且没有状态标记是否发起过请求,导致组件第一次渲染时,会错误显示“加载完成”的内容(如示例中的Hello world!),直到useEffect触发fetch后loading才变为true。本质是缺少“未发起请求”的状态区分,当前loading仅能表示“是否正在加载”,无法区分“从未加载过”和“加载完成”两种状态。


解决方案

方案1:新增hasFetched状态标记请求触发状态

在Hook中新增hasFetched状态,明确标记是否已经发起过至少一次数据请求。手动模式下初始值为false,自动模式下初始值为true(因为自动模式会在挂载时立即发起请求)。

修改后的Hook代码:

const useOrders = (manual: boolean) => {
  const [{ loading, error, data, hasFetched }, setState] = useState<{
    loading: boolean,
    error: null | unknown,
    data: Order[],
    hasFetched: boolean
  }>({ 
    loading: !manual, 
    error: null, 
    data: [],
    hasFetched: !manual // 自动模式默认已触发请求,手动模式初始未触发
  });

  const fetch = useCallback(async () => {
    setState(prev => ({ 
      ...prev, 
      loading: true, 
      error: null,
      hasFetched: true // 发起请求时标记为已触发
    }));

    return axios.get<Order[]>('/api/orders')
      .then(({ data }) => {
        setState(prev => ({ ...prev, loading: false, data }));
        return data;
      })
      .catch(error => {
        setState(prev => ({ ...prev, loading: false, error }));
        throw error;
      });
  }, []);

  useEffect(() => {
    !manual && fetch();
  }, [manual]);

  return { fetch, loading, error, data, hasFetched };
}

组件渲染逻辑调整:

const Modal = () => {
  const { fetch, loading, data, error, hasFetched } = useOrders(true);
  
  useEffect(() => {
    isOpen && fetch();
  }, [isOpen]);

  return (
    <>
      {loading && <p>Loading...</p>}
      {/* 仅当已触发请求、未加载且无错误时,才显示内容 */}
      {!loading && hasFetched && !error && <p>Hello world!</p>}
    </>
  );
};

方案2:通过data初始值区分状态

如果不想新增额外状态,可以调整data的类型,将手动模式下的初始值设为undefined,用来表示“从未发起过请求”,请求完成后再设置为数组(空或有数据)。

修改后的Hook代码:

const useOrders = (manual: boolean) => {
  const [{ loading, error, data }, setState] = useState<{
    loading: boolean,
    error: null | unknown,
    data: Order[] | undefined // 修改data类型为可选数组
  }>({ 
    loading: !manual, 
    error: null, 
    data: manual ? undefined : [] // 手动模式初始为undefined,自动模式初始为空数组
  });

  const fetch = useCallback(async () => {
    setState(prev => ({ ...prev, loading: true, error: null }));

    return axios.get<Order[]>('/api/orders')
      .then(({ data }) => {
        setState(prev => ({ ...prev, loading: false, data }));
        return data;
      })
      .catch(error => {
        setState(prev => ({ ...prev, loading: false, error }));
        throw error;
      });
  }, []);

  useEffect(() => {
    !manual && fetch();
  }, [manual]);

  return { fetch, loading, error, data };
}

组件渲染逻辑调整:

const Modal = () => {
  const { fetch, loading, data, error } = useOrders(true);
  
  useEffect(() => {
    isOpen && fetch();
  }, [isOpen]);

  return (
    <>
      {loading && <p>Loading...</p>}
      {/* 仅当data存在(即已发起请求)、未加载且无错误时,才显示内容 */}
      {data !== undefined && !loading && !error && <p>Hello world!</p>}
    </>
  );
};

方案说明

两种方案都通过明确区分“未发起请求”和“加载完成”状态,解决了手动模式下的渲染间隙问题:

  • 方案1语义更清晰,新增状态直接标记请求触发情况;
  • 方案2无需额外状态,通过data初始值区分,但需要调整类型定义。

内容的提问来源于stack exchange,提问作者gremo

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 06:35:18