Redux中关联数据删除如何处理?避免用redux-thunk获取状态
Great question! I totally get why you'd want to avoid using redux-thunk just to read state for this operation—it does feel like overkill or even an anti-pattern when the only reason for using it is to fetch related data before dispatching actions. Let's walk through several solid alternatives, including whether redux-saga can help here:
1. Pass Required Child IDs Directly from the Component
Instead of fetching child IDs inside a thunk, have your component retrieve the child IDs via a selector and pass them directly to the action creator. This keeps your action creators pure and avoids needing thunk just for state access.
// Updated action creator that accepts both parentId and childrenIds function deleteParentAndChildren(parentId, childrenIds) { return batchActions([ deleteParents(parentId), ...childrenIds.map(deleteChild) ], 'DELETE_PARENT_AND_CHILDREN'); } // Usage in a component const ParentComponent = ({ parentId }) => { const dispatch = useDispatch(); // Get child IDs via selector right in the component (this is a valid use of selectors!) const childrenIds = useSelector(state => myChildrenSelector(state, parentId)); const handleDelete = () => { dispatch(deleteParentAndChildren(parentId, childrenIds)); }; return <button onClick={handleDelete}>Delete Parent & Children</button>; };
This approach is simple and keeps logic where it belongs: components read state, action creators prepare actions, and reducers update state.
2. Handle Association Deletion Directly in the Reducer
Since deleting a parent inherently requires deleting its children (a strong business rule), you can handle both operations in the same reducer case for DELETE_PARENT. This eliminates the need for multiple actions entirely.
If you're using vanilla Redux:
function rootReducer(state = initialState, action) { switch (action.type) { case 'DELETE_PARENT': { const { parentId } = action.payload; const targetParent = state.parent.byId[parentId]; if (!targetParent) return state; const childrenToDelete = targetParent.children; return { ...state, parent: { ...state.parent, allIds: state.parent.allIds.filter(id => id !== parentId), byId: Object.fromEntries( Object.entries(state.parent.byId).filter(([id]) => id !== parentId) ) }, children: { ...state.children, allIds: state.children.allIds.filter(id => !childrenToDelete.includes(id)), byId: Object.fromEntries( Object.entries(state.children.byId).filter(([id]) => !childrenToDelete.includes(Number(id))) ) } }; } // Other cases... default: return state; } }
If you're using Redux Toolkit (the official recommended approach), this becomes even cleaner with Immer:
import { createSlice } from '@reduxjs/toolkit'; const entitiesSlice = createSlice({ name: 'entities', initialState, reducers: { deleteParent: (state, action) => { const parentId = action.payload.parentId; const parent = state.parent.byId[parentId]; if (!parent) return; // Delete the parent delete state.parent.byId[parentId]; state.parent.allIds = state.parent.allIds.filter(id => id !== parentId); // Delete all associated children parent.children.forEach(childId => { delete state.children.byId[childId]; }); state.children.allIds = state.children.allIds.filter(id => !parent.children.includes(id)); } } }); export const { deleteParent } = entitiesSlice.actions; export default entitiesSlice.reducer;
Now you just dispatch deleteParent(parentId) and the reducer handles the rest—no thunks or sagas needed.
3. Use Redux-Saga for Centralized Side Effects
Yes, redux-saga is absolutely helpful here! Sagas excel at handling operations that require reading state, coordinating multiple actions, or managing complex workflows. They keep your components and action creators clean by moving this logic to a centralized, testable location.
Here's how you'd implement it:
// 1. Create a trigger action function deleteParentAndChildrenRequest(parentId) { return { type: 'DELETE_PARENT_AND_CHILDREN_REQUEST', payload: { parentId } }; } // 2. Write the saga to handle the request import { takeLatest, select, put, all } from 'redux-saga/effects'; function* handleDeleteParentAndChildren(action) { const { parentId } = action.payload; // Read state directly in the saga const state = yield select(); const childrenIds = myChildrenSelector(state, parentId); // Dispatch all delete actions in parallel yield all([ put(deleteParents(parentId)), ...childrenIds.map(childId => put(deleteChild(childId))) ]); } // 3. Register the saga listener function* rootSaga() { yield takeLatest('DELETE_PARENT_AND_CHILDREN_REQUEST', handleDeleteParentAndChildren); }
In your component, you just dispatch the request action:
dispatch(deleteParentAndChildrenRequest(parentId));
Sagas shine if you ever need to add more complexity later—like showing a confirmation dialog, handling API errors, or retrying failed deletions—without cluttering your components or reducers.
Which Should You Choose?
- If you want minimal overhead: Go with reducer-side association deletion (especially with Redux Toolkit) or passing child IDs from the component.
- If you need workflow/side effect management: Redux-saga is a great choice for centralized, testable logic.
All these approaches avoid using redux-thunk solely for state access, keeping your Redux code clean and aligned with best practices.
内容的提问来源于stack exchange,提问作者ttncrch

