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

如何在use-immer reducer中结合Promise正确管理API请求与状态?

解决Next.js开发模式下Hook重复调用与Reducer整合Fetcher的问题

核心问题分析

你遇到的卡点本质是Reducer是纯函数,不能直接在内部执行异步操作并修改后续状态——Immer的draft仅在当前Reducer执行周期内有效,异步回调里的draft已经不是最新的状态引用,直接修改不会触发组件更新,甚至会引发状态不一致。

解决方案

要把异步逻辑和Reducer状态管理解耦,同时保留自定义Fetcher的去重能力,推荐以下两种落地方案:

方案一:组件层处理异步,通过dispatch更新状态

把API调用移到组件或自定义Hook中,Fetcher的fetchMap仍由Reducer统一管理,异步完成后通过action更新状态:

1. 调整Reducer逻辑

修改dashboardReducer.js,新增处理异步结果的action类型:

export function dashboardReducer(draft, action) {
  switch (action.type) {
    case 'GET_ROWS_START': {
      // 可选:标记请求开始,用于展示加载状态
      break;
    }
    case 'GET_ROWS_SUCCESS': {
      draft.tableRows = action.data;
      delete draft.fetchMap['/api/getRows'];
      break;
    }
    case 'ADD_ROW_START': {
      // 乐观更新本地状态
      draft.tableRows.push(action.data);
      break;
    }
    case 'ADD_ROW_SUCCESS': {
      delete draft.fetchMap['/api/addRow'];
      // 可选:如果需要同步后端最新数据,可在此重新获取或更新
      break;
    }
    case 'SET_FETCH_PROMISE': {
      // 将请求Promise存入fetchMap,保证去重
      draft.fetchMap[action.url] = action.promise;
      break;
    }
  }
}

2. 组件层封装异步调用

修改component.js,把API调用移到组件内,利用state.fetchMap实现请求去重:

import { useImmerReducer } from 'use-immer';
import dashboardReducer from './dashboardReducer.js';
import { createFetch } from './fetcher.js';

export default function MyDashboard({}) {
  const [state, dispatch] = useImmerReducer(dashboardReducer, {
    fetchMap: {},
    tableRows: [],
  });

  const handleAddRow = (value) => {
    // 先更新本地状态(乐观更新)
    dispatch({ type: 'ADD_ROW_START', data: value });
    // 调用API并获取Promise
    const promise = createFetch(state.fetchMap, '/api/addRow', {
      method: 'POST',
      body: JSON.stringify({ value }),
      headers: { 'Content-Type': 'application/json' },
    });
    // 把Promise存入fetchMap确保去重
    dispatch({ type: 'SET_FETCH_PROMISE', url: '/api/addRow', promise });
    // 请求完成后更新状态
    promise.then(() => {
      dispatch({ type: 'ADD_ROW_SUCCESS' });
    });
  };

  const getRows = () => {
    dispatch({ type: 'GET_ROWS_START' });
    const promise = createFetch(state.fetchMap, '/api/getRows', {});
    dispatch({ type: 'SET_FETCH_PROMISE', url: '/api/getRows', promise });
    promise.then((data) => {
      dispatch({ type: 'GET_ROWS_SUCCESS', data });
    });
  };

  return (
    <>
      <button onClick={() => handleAddRow('banana')}>peel</button>
      {state.tableRows.map((banana, idx) => (
        <p key={idx}>{banana}</p>
      ))}
    </>
  );
}

3. 优化Fetcher(可选)

让Fetcher自动清理fetchMap,避免内存泄漏:

export function createFetch(fetchMap, url, options) {
  if (!fetchMap[url]) {
    const promise = fetch(url, options)
      .then((res) => res.json())
      .finally(() => {
        // 请求完成后自动移除,保证后续请求可正常发起
        delete fetchMap[url];
      });
    fetchMap[url] = promise;
  }
  return fetchMap[url];
}

方案二:使用Redux Toolkit(复杂项目推荐)

如果你的状态逻辑越来越复杂,推荐用Redux Toolkit的createAsyncThunk,它原生支持异步逻辑与状态管理的整合,同时可结合自定义Fetcher实现去重:

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { createFetch } from './fetcher.js';

// 封装异步请求
export const getRows = createAsyncThunk('dashboard/getRows', async (_, { getState }) => {
  const { fetchMap } = getState().dashboard;
  return createFetch(fetchMap, '/api/getRows', {});
});

export const addRow = createAsyncThunk('dashboard/addRow', async (value, { getState }) => {
  const { fetchMap } = getState().dashboard;
  return createFetch(fetchMap, '/api/addRow', {
    method: 'POST',
    body: JSON.stringify({ value }),
    headers: { 'Content-Type': 'application/json' },
  });
});

// 创建Slice管理状态
const dashboardSlice = createSlice({
  name: 'dashboard',
  initialState: {
    fetchMap: {},
    tableRows: [],
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(getRows.fulfilled, (state, action) => {
        state.tableRows = action.payload;
        delete state.fetchMap['/api/getRows'];
      })
      .addCase(addRow.pending, (state, action) => {
        // 乐观更新本地状态
        state.tableRows.push(action.meta.arg);
      })
      .addCase(addRow.fulfilled, (state) => {
        delete state.fetchMap['/api/addRow'];
      });
  },
});

export default dashboardSlice.reducer;

关键注意事项

  • Reducer必须是纯函数:永远不要在Reducer里执行异步操作、修改全局变量或调用副作用函数,所有异步逻辑都要放在组件或专门的异步处理函数中。
  • fetchMap的清理:一定要在请求完成(成功/失败)后清理fetchMap,避免内存泄漏,同时保证后续请求可以正常发起。
  • 乐观更新的一致性:如果使用乐观更新,要处理请求失败的情况,及时回滚本地状态。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 06:54:57