如何在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
相关产品推荐
相关产品推荐

