You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Redux中实现Expander功能,基于API扩展现有状态数据?

How to Append API-Fetched Details to Existing Redux List Items

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 08:17:55