Redux reducer与服务端API协作及redux-actions的reducer创建最佳实践
Hey there! Let's dive into your Redux questions — I’ve got plenty of hands-on experience with redux-actions and API integrations, so I’ll share practical, actionable approaches here.
First off, remember that reducers are pure functions — they can’t perform side effects like API calls directly. So we need to use middleware to handle async logic, then feed the results back to reducers via plain action objects. Here’s a standard, battle-tested workflow:
Split responsibilities clearly:
- Reducers: Only update state immutably based on sync action objects (no async code allowed here).
- Async logic: Handled in action creators (with middleware like
redux-thunk) or dedicated async layers (likeredux-sagafor more complex flows).
Practical example with redux-thunk:
Let’s say you want to fetch a list of items from an API. First, define your action types and creators (usingredux-actionsfor consistency):import { createAction } from 'redux-actions'; // Action types const FETCH_ITEMS_REQUEST = 'FETCH_ITEMS_REQUEST'; const FETCH_ITEMS_SUCCESS = 'FETCH_ITEMS_SUCCESS'; const FETCH_ITEMS_FAILURE = 'FETCH_ITEMS_FAILURE'; // Sync action creators const fetchItemsRequest = createAction(FETCH_ITEMS_REQUEST); const fetchItemsSuccess = createAction(FETCH_ITEMS_SUCCESS); const fetchItemsFailure = createAction(FETCH_ITEMS_FAILURE); // Async action creator (thunk) export const fetchItems = () => async (dispatch) => { dispatch(fetchItemsRequest()); // Signal we're starting the request try { const response = await fetch('/api/items'); const data = await response.json(); dispatch(fetchItemsSuccess(data)); // Pass data to reducer on success } catch (error) { dispatch(fetchItemsFailure(error.message)); // Pass error on failure } };Then your reducer handles these actions to update state:
import { handleActions } from 'redux-actions'; const initialState = { items: [], loading: false, error: null }; const itemsReducer = handleActions( { [FETCH_ITEMS_REQUEST]: (state) => ({ ...state, loading: true, error: null }), [FETCH_ITEMS_SUCCESS]: (state, { payload }) => ({ ...state, loading: false, items: payload }), [FETCH_ITEMS_FAILURE]: (state, { payload }) => ({ ...state, loading: false, error: payload }) }, initialState );Core flow:
- Component dispatches the async action (
fetchItems()). - Thunk middleware runs the async logic, dispatching a "request" action first to set loading state.
- On API success/failure, the thunk dispatches a "success" or "failure" action.
- Reducer responds to these plain actions, updating the state immutably.
- Component re-renders with the updated state.
- Component dispatches the async action (
If you need more control over async flows (like canceling requests or debouncing), redux-saga is a great alternative — but the core idea remains the same: async logic lives outside reducers, and reducers only handle state updates from sync actions.
handleActions in redux-actions Both approaches you mentioned have their use cases — let’s walk through each with concrete examples.
(1) Separate Reducers for Each CRUD Operation, Then Combine Them
This approach works best when your CRUD operations have distinct state concerns (e.g., adding state vs. deleting state don’t overlap much). It keeps each reducer focused, easier to test, and simpler to debug.
How to Set initialState:
Each individual reducer manages its own slice of the state, so you define initialState per reducer, matching exactly what that reducer needs to track. Then use combineReducers to merge them into a root reducer.
Example:
import { handleActions, createAction } from 'redux-actions'; import { combineReducers } from 'redux'; // Action types for CRUD const ADD_ITEM_REQUEST = 'ADD_ITEM_REQUEST'; const ADD_ITEM_SUCCESS = 'ADD_ITEM_SUCCESS'; const DELETE_ITEM_REQUEST = 'DELETE_ITEM_REQUEST'; const DELETE_ITEM_SUCCESS = 'DELETE_ITEM_SUCCESS'; // Action creators export const addItemRequest = createAction(ADD_ITEM_REQUEST); export const addItemSuccess = createAction(ADD_ITEM_SUCCESS); export const deleteItemRequest = createAction(DELETE_ITEM_REQUEST); export const deleteItemSuccess = createAction(DELETE_ITEM_SUCCESS); // Add Item Reducer (tracks add-specific state) const addInitialState = { isAdding: false, addError: null }; const addReducer = handleActions( { [ADD_ITEM_REQUEST]: (state) => ({ ...state, isAdding: true, addError: null }), [ADD_ITEM_SUCCESS]: (state) => ({ ...state, isAdding: false }) }, addInitialState ); // Delete Item Reducer (tracks delete-specific state) const deleteInitialState = { deletingId: null, deleteError: null }; const deleteReducer = handleActions( { [DELETE_ITEM_REQUEST]: (state, { payload }) => ({ ...state, deletingId: payload }), [DELETE_ITEM_SUCCESS]: (state) => ({ ...state, deletingId: null }) }, deleteInitialState ); // Data Reducer (manages the actual list of items) const dataInitialState = []; const dataReducer = handleActions( { [ADD_ITEM_SUCCESS]: (state, { payload }) => [...state, payload], [DELETE_ITEM_SUCCESS]: (state, { payload }) => state.filter(item => item.id !== payload) }, dataInitialState ); // Combine all reducers into a root reducer export const rootReducer = combineReducers({ add: addReducer, delete: deleteReducer, items: dataReducer });
Why this works:
- Each reducer has a single responsibility (e.g.,
addReduceronly tracks add-related loading/error state). initialStateis scoped to what each reducer needs, avoiding bloated, hard-to-maintain state objects.- Easier to test — you can test each reducer in isolation without worrying about unrelated state.
(2) Single Reducer for All Related CRUD Actions
This approach is better for smaller features where your CRUD operations share closely related state (e.g., you want to track a single loading state, or all actions affect the same data list). It keeps all logic for a feature centralized in one place.
Example:
import { handleActions, createAction } from 'redux-actions'; // Action types const FETCH_DELETE_DATA_REQUEST = 'FETCH_DELETE_DATA_REQUEST'; const FETCH_DELETE_DATA_SUCCESS = 'FETCH_DELETE_DATA_SUCCESS'; const FETCH_ADD_DATA_REQUEST = 'FETCH_ADD_DATA_REQUEST'; const FETCH_ADD_DATA_SUCCESS = 'FETCH_ADD_DATA_SUCCESS'; // Action creators export const fetchDeleteDataRequest = createAction(FETCH_DELETE_DATA_REQUEST); export const fetchDeleteDataSuccess = createAction(FETCH_DELETE_DATA_SUCCESS); export const fetchAddDataRequest = createAction(FETCH_ADD_DATA_REQUEST); export const fetchAddDataSuccess = createAction(FETCH_ADD_DATA_SUCCESS); // Combined reducer for all CRUD actions const initialState = { data: [], isLoading: false, deletingId: null, // Track which item is being deleted addingItem: null // Track which item is being added }; const crudReducer = handleActions( { [FETCH_ADD_DATA_REQUEST]: (state, { payload }) => ({ ...state, isLoading: true, addingItem: payload }), [FETCH_ADD_DATA_SUCCESS]: (state, { payload }) => ({ ...state, isLoading: false, addingItem: null, data: [...state.data, payload] }), [FETCH_DELETE_DATA_REQUEST]: (state, { payload }) => ({ ...state, isLoading: true, deletingId: payload }), [FETCH_DELETE_DATA_SUCCESS]: (state, { payload }) => ({ ...state, isLoading: false, deletingId: null, data: state.data.filter(item => item.id !== payload) }) }, initialState ); export default crudReducer;
Why this works:
- All logic for the feature is centralized — easy to see how each action affects the state at a glance.
- Reduces boilerplate for small features where splitting into multiple reducers would add unnecessary complexity.
- You can share state between actions (e.g., a single
isLoadingflag for all CRUD operations, if that makes sense for your UI).
内容的提问来源于stack exchange,提问作者VladislavKh

