React useReducer异步数据获取方案的正确性及优化咨询
方案正确性判断
你的实现是完全可用的,不过首先要纠正一个常见的认知误区:React 中 useReducer 的 dispatch 本身并不是异步执行的,只是 React 会对同一个事件循环内的状态更新做批量处理,导致你无法在调用 dispatch 后立刻读取到最新的 state,但是 dispatch 触发 reducer 计算新状态、调度组件重渲染的动作是同步触发的。
你用 useEffect 监听 state.loading 变化再触发请求的写法,确实严格保证了只有加载状态生效后才会启动异步请求,完全规避了状态批量处理的潜在时序问题,逻辑上没有问题。不过这套实现存在两个可以优化的点:
- 代码冗余,多了一层不必要的副作用监听逻辑
- 如果后续有其他逻辑修改
loading状态,很容易意外触发重复的请求,而且你当前代码中同时保留了randomizeNumber异步函数和useEffect两套请求逻辑,使用不当会出现重复请求的问题。
更优的实现方案
1. 最简方案(适用于简单场景)
其实你注释里写的原方案就完全够用,不会出现加载动画无法按时展示的问题:
import { useReducer } from 'react'; const initialState = { number: 0, loading: false }; function reducer(state, action) { switch (action.type) { case 'loading': return { ...state, loading: true }; case 'completed': return { number: +action.payload, loading: false }; default: return state; } } function genRand100() { return new Promise(resolve => { setTimeout(() => resolve(1 + Math.floor(Math.random() * 100)), 1000); }); } export default function App() { const [state, dispatch] = useReducer(reducer, initialState); const randomizeNumber = async () => { dispatch({ type: 'loading' }); const number = await genRand100(); dispatch({ type: 'completed', payload: number }); }; return ( <div style={{ textAlign: 'center' }}> {state.loading ? <h1>Generating ...</h1> : <h1>{state.number}</h1>} <button onClick={randomizeNumber}>Generate random number</button> </div> ); }
这个写法的时序是有JS事件循环机制保障的:dispatch 触发的重渲染会在当前同步代码执行完成后立刻执行,而 await 后面的异步请求属于微任务/宏任务,会在重渲染完成之后才执行,所以 loading 状态一定会先更新,加载动画一定会正常展示,不存在时序问题。
2. 易扩展方案(适用于中大型项目)
如果你的项目异步逻辑较多,想要做逻辑解耦和复用,可以参考 redux-thunk 的思路,自定义一个支持异步 action 的 useThunkReducer 钩子:
const useThunkReducer = (reducer, initialState) => { const [state, dispatch] = useReducer(reducer, initialState); const thunkDispatch = (action) => { if (typeof action === 'function') { return action(thunkDispatch, () => state); } return dispatch(action); }; return [state, thunkDispatch]; };
之后你就可以把异步逻辑封装成独立的 action creator,复用性更高:
// 异步action可以单独抽离到别的文件 const fetchRandomNumber = () => async (dispatch) => { dispatch({ type: 'loading' }); const number = await genRand100(); dispatch({ type: 'completed', payload: number }); }; export default function App() { const [state, dispatch] = useThunkReducer(reducer, initialState); return ( <div style={{ textAlign: 'center' }}> {state.loading ? <h1>Generating ...</h1> : <h1>{state.number}</h1>} <button onClick={() => dispatch(fetchRandomNumber())}>Generate random number</button> </div> ); }
如果是更复杂的项目,也可以直接使用成熟的状态管理库比如 Redux Toolkit、Zustand 等,这些库都内置了异步处理能力,不需要自己封装基础逻辑。
内容的提问来源于stack exchange,提问作者Aditya Verma

