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

React 18中如何正确获取数据并存入Zustand Store

React 18 严格模式下配合Zustand实现数据请求的正确方式

首先明确核心逻辑:React 18严格模式开发环境下useEffect双执行,本质是框架主动帮你排查缺少清理逻辑的副作用问题,不是禁止在useEffect中写数据请求逻辑。你之前看到的"不建议在useEffect中获取数据"的说法,本质是反对不带任何竞态处理、清理逻辑的裸请求写法。

你之前React 17的写法问题在于没有做请求去重、中断和过期请求拦截,才会在双调用时出现重复发请求、状态异常的问题,只需要补上对应逻辑即可,不需要完全重构现有代码。


方案1:最小改动适配:给Zustand Action补全请求防重+取消逻辑

这个方案完全兼容你现有的代码组织方式,只需要改造store的请求action,同时给组件useEffect补上清理函数即可。

第一步:改造Zustand Store

核心是给请求加上AbortController支持,重复触发请求时自动中断上一次未完成的请求,同时过滤掉主动取消的请求错误,避免误更新状态:

import { create } from 'zustand';
import axios from 'axios';

const useStore = create((set, get) => ({
  items: [],
  loading: false,
  error: null,
  // 内部存储当前请求的取消控制器,不需要对外暴露给组件
  _fetchAbortController: null,
  async fetchItems(page = 1) {
    // 存在未完成的请求时先主动中断,避免重复请求
    const existingController = get()._fetchAbortController;
    if (existingController) existingController.abort();

    // 为本次请求创建新的取消控制器
    const newController = new AbortController();
    set({
      _fetchAbortController: newController,
      loading: true,
      error: null
    });

    try {
      const { data } = await axios.get(`my-url?page=${page}`, {
        signal: newController.signal // 把取消信号传给axios
      });
      // 只有当前请求未被中断时才更新状态,拦截过期请求的返回
      if (!newController.signal.aborted) {
        set({
          loading: false,
          items: data,
          _fetchAbortController: null
        });
      }
    } catch (err) {
      // 主动取消的请求不需要更新错误、loading状态
      if (err.name === 'CanceledError' || axios.isCancel(err)) return;
      set({
        error: "There was an error.",
        loading: false,
        _fetchAbortController: null
      });
    }
  }
}));

第二步:调整组件useEffect逻辑,补上清理函数

组件的业务逻辑几乎不需要改动,只需要在useEffect的返回值里加上请求清理逻辑,在effect重跑、组件卸载时自动中断未完成的请求:

function MyComponent() {
  const items = useStore(state => state.items);
  const loading = useStore(state => state.loading);
  const error = useStore(state => state.error);
  const fetchItems = useStore(state => state.fetchItems);

  useEffect(() => {
    fetchItems(1);
    // 清理函数:effect销毁前中断未完成的请求
    return () => {
      const currentController = useStore.getState()._fetchAbortController;
      currentController?.abort();
    };
  }, [fetchItems]);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>{error}</div>;

  return (
    <div>
      {items.map(item => <span key={item.id}>{item.name}</span>)}
    </div>
  );
}

这个写法在严格模式下的执行逻辑是:

  • 组件首次挂载,第一个effect执行,发起第一次请求
  • 严格模式触发effect销毁,执行清理函数中断第一次请求
  • 第二个effect执行,发起正常业务请求,最终只有这一次有效请求会更新状态

完全不会出现重复请求、状态错乱的问题,同时这套逻辑在生产环境、非严格模式、组件快速重挂载、请求参数频繁变化的场景下都能正常工作,从根源上解决了请求竞态问题。


避坑提醒

不要用useRef存布尔标记位(比如存const mounted = useRef(false),判断mounted状态再发请求)的写法来规避双调用:这种写法只能挡住严格模式下的开发环境双调用,遇到组件正常重挂载、请求参数变化导致的effect重跑、慢请求覆盖新请求等场景,还是会出现竞态bug,属于治标不治本的歪门邪道。


可选优化方案(适合中大型项目)

如果项目里有大量列表请求、分页、缓存需求,可以直接用TanStack Query和Zustand配合,把请求去重、缓存、重试、取消、竞态处理的逻辑交给成熟库处理,不需要自己手动维护AbortController和请求状态,代码会更简洁。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 04:33:25