如何在Redux中实现Expander功能,基于API扩展现有状态数据?
Absolutely, you can append fetched details to your existing list items without overhauling your Redux structure. The key is to create a targeted action for fetching these details, then update your reducer to merge the new data into the corresponding list entry instead of replacing the entire state. Let’s walk through a concrete, actionable example.
Step 1: Create an Async Action to Fetch Details
First, we’ll build an action creator that uses Axios to fetch details for a specific item ID, then dispatches success/loading/error actions. We’ll use Redux Thunk here (common for async operations) to handle the API call:
// actions.js import axios from 'axios'; // Action types export const FETCH_ITEM_DETAILS_SUCCESS = 'FETCH_ITEM_DETAILS_SUCCESS'; export const FETCH_ITEM_DETAILS_LOADING = 'FETCH_ITEM_DETAILS_LOADING'; export const FETCH_ITEM_DETAILS_ERROR = 'FETCH_ITEM_DETAILS_ERROR'; // Async action creator export const fetchItemDetails = (itemId) => async (dispatch) => { // Mark item as loading dispatch({ type: FETCH_ITEM_DETAILS_LOADING, payload: itemId }); try { const response = await axios.get(`/api/items/${itemId}/details`); dispatch({ type: FETCH_ITEM_DETAILS_SUCCESS, payload: { itemId, details: response.data // The fetched details to append } }); } catch (error) { dispatch({ type: FETCH_ITEM_DETAILS_ERROR, payload: { itemId, error: error.message } }); } };
Step 2: Update Your Reducer to Merge Details
Next, modify your existing reducer to handle these new actions. The critical part is mapping over your existing items array, finding the matching item by ID, and merging the new details into it (while keeping immutability intact):
Assuming your initial state looks like this:
const initialState = { items: [ { id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' } // ... other existing items ], loadingItems: [], // Track which items are loading errorItems: {} // Track errors per item };
Here’s the updated reducer:
// reducer.js import { FETCH_ITEM_DETAILS_SUCCESS, FETCH_ITEM_DETAILS_LOADING, FETCH_ITEM_DETAILS_ERROR } from './actions'; const itemsReducer = (state = initialState, action) => { switch (action.type) { case FETCH_ITEM_DETAILS_LOADING: return { ...state, loadingItems: [...state.loadingItems, action.payload] }; case FETCH_ITEM_DETAILS_SUCCESS: const { itemId, details } = action.payload; return { ...state, // Map over items to find and update the matching one items: state.items.map(item => item.id === itemId ? { ...item, details, isLoading: false, error: null } // Merge existing item with new details : item ), loadingItems: state.loadingItems.filter(id => id !== itemId) }; case FETCH_ITEM_DETAILS_ERROR: const { itemId: errorId, error } = action.payload; return { ...state, items: state.items.map(item => item.id === errorId ? { ...item, error, isLoading: false } : item ), loadingItems: state.loadingItems.filter(id => id !== errorId), errorItems: { ...state.errorItems, [errorId]: error } }; // Keep your existing reducer cases (e.g., for fetching the initial list) here default: return state; } }; export default itemsReducer;
Step 3: Dispatch the Action for Each List Item
In your component, use useEffect to loop through your existing items and dispatch the fetch action for each (only if details don’t already exist to avoid redundant calls):
// ItemListComponent.js import { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { fetchItemDetails } from './actions'; const ItemListComponent = () => { const dispatch = useDispatch(); const { items } = useSelector(state => state.items); useEffect(() => { items.forEach(item => { // Skip if details are already loaded if (!item.details) { dispatch(fetchItemDetails(item.id)); } }); }, [dispatch, items]); return ( <div className="item-list"> {items.map(item => ( <div key={item.id} className="item-card"> <h3>{item.name}</h3> {item.isLoading && <p>Loading details...</p>} {item.error && <p className="error">Error: {item.error}</p>} {item.details && ( <div className="item-details"> <p>Description: {item.details.description}</p> <p>Price: ${item.details.price}</p> {/* Render other fetched details here */} </div> )} </div> ))} </div> ); }; export default ItemListComponent;
Bonus: Redux Toolkit Shortcut
If you’re using Redux Toolkit (modern Redux’s recommended approach), this becomes even cleaner thanks to Immer (which handles immutability automatically) and createAsyncThunk:
// slices/itemsSlice.js import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; import axios from 'axios'; export const fetchItemDetails = createAsyncThunk( 'items/fetchItemDetails', async (itemId) => { const response = await axios.get(`/api/items/${itemId}/details`); return { itemId, details: response.data }; } ); const itemsSlice = createSlice({ name: 'items', initialState: { items: [], loadingItems: [], errorItems: {} }, reducers: { // Your existing reducers here }, extraReducers: (builder) => { builder .addCase(fetchItemDetails.pending, (state, action) => { state.loadingItems.push(action.meta.arg); }) .addCase(fetchItemDetails.fulfilled, (state, action) => { const { itemId, details } = action.payload; const item = state.items.find(i => i.id === itemId); if (item) { item.details = details; item.isLoading = false; item.error = null; } state.loadingItems = state.loadingItems.filter(id => id !== itemId); }) .addCase(fetchItemDetails.rejected, (state, action) => { const itemId = action.meta.arg; const item = state.items.find(i => i.id === itemId); if (item) { item.error = action.error.message; item.isLoading = false; } state.loadingItems = state.loadingItems.filter(id => id !== itemId); state.errorItems[itemId] = action.error.message; }); } }); export default itemsSlice.reducer;
This approach eliminates manual spread operators and makes the code more readable.
内容的提问来源于stack exchange,提问作者tcoulson

