React新手咨询:使用多useReducer处理同一份Todo数据是否合理
你这个思路非常棒——把通用的数据获取逻辑和Todo业务操作拆分到不同的Reducer里,既符合单一职责原则,又能让dataFetchReducer真正实现复用,这正是React Hooks设计的初衷之一。下面我给你两种具体的实现方案,你可以根据自己的习惯选择:
方案一:组合Reducer(类似Redux的combineReducers)
这种方式适合希望保持单一状态树和单一dispatch的场景,RootReducer会根据Action类型自动分发给对应的子Reducer处理。
1. 编写Todo业务Reducer
先实现处理Todo操作的todoReducer,专注于reset、add、remove、markAsDone这些业务逻辑:
const todoReducer = (state, action) => { switch (action.type) { case 'TODO_RESET': return { ...state, data: [] }; case 'TODO_ADD': return { ...state, data: [...state.data, action.payload] }; case 'TODO_REMOVE': return { ...state, data: state.data.filter(todo => todo.id !== action.payload.id) }; case 'TODO_MARK_AS_DONE': return { ...state, data: state.data.map(todo => todo.id === action.payload.id ? { ...todo, done: true } : todo ) }; default: // 如果不是Todo相关的Action,直接返回原状态 return state; } };
2. 组合RootReducer
创建一个RootReducer,把dataFetchReducer和todoReducer组合起来。它会先让dataFetchReducer处理数据获取相关的Action(比如FETCH_INIT/SUCCESS/FAILURE),再判断是否是Todo相关的Action,交给todoReducer处理:
const rootReducer = (state, action) => { // 先处理数据获取的Action const fetchUpdatedState = dataFetchReducer(state, action); // 如果是Todo业务Action,交给todoReducer处理 if (action.type.startsWith('TODO_')) { return todoReducer(fetchUpdatedState, action); } return fetchUpdatedState; };
3. 使用RootReducer初始化状态
现在只需要用RootReducer来创建状态和dispatch,所有Action都通过这个dispatch发送即可:
const initialData = { isLoading: true, isError: false, data: [] }; const [todoState, dispatch] = useReducer(rootReducer, initialData);
比如你要添加一个Todo,直接调用:
dispatch({ type: 'TODO_ADD', payload: { id: 1, text: 'Learn useReducer', done: false } });
获取数据的时候还是用原来的FETCH_系列Action,完全不影响dataFetchReducer的复用。
方案二:使用多个useReducer分离状态
如果希望把「数据获取元状态(loading/error)」和「Todo业务数据」完全分开管理,可以用两个独立的useReducer,分别对应不同的状态和dispatch(变量名区分开就不会冲突)。
1. 拆分状态并初始化两个Reducer
// 专门管理数据获取的状态(loading/error) const initialFetchState = { isLoading: true, isError: false }; const [fetchState, fetchDispatch] = useReducer(dataFetchReducer, initialFetchState); // 专门管理Todo业务数据 const initialTodoData = []; const [todoData, todoDispatch] = useReducer(todoReducer, initialTodoData);
2. 同步Fetch到的Todo数据
在数据获取成功后,除了更新fetch状态,还要把数据同步到Todo的状态里(需要给todoReducer加一个TODO_SET_DATA的Action):
// 在useEffect里发起请求 useEffect(() => { fetchDispatch({ type: 'FETCH_INIT' }); fetch('/api/todos') .then(res => res.json()) .then(data => { fetchDispatch({ type: 'FETCH_SUCCESS', payload: data }); // 同步数据到Todo状态 todoDispatch({ type: 'TODO_SET_DATA', payload: data }); }) .catch(() => { fetchDispatch({ type: 'FETCH_FAILURE' }); }); }, []);
对应的todoReducer要加上这个Action的处理:
const todoReducer = (state, action) => { switch (action.type) { case 'TODO_SET_DATA': return action.payload; // 其他Todo操作的case... default: return state; } };
这种方式的好处是状态边界更清晰,每个Reducer只关注自己的逻辑,dataFetchReducer同样可以直接拿到其他组件里复用。
两种方案怎么选?
- 如果你的组件需要把loading/error状态和Todo数据一起渲染(比如加载中时显示骨架屏),方案一更方便,因为状态是统一的;
- 如果想让状态完全解耦,或者未来Todo数据可能有更复杂的独立逻辑,方案二会更灵活。
不管选哪种,你的核心思路——拆分Reducer实现复用和职责分离——都是完全正确的,继续保持这个好习惯!
内容的提问来源于stack exchange,提问作者dinker

