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

如何在Redux(Ducks模式)中避免重复代码?

Hey there! It's totally relatable to hit this repetition wall when using the Ducks pattern across multiple feature modules—great catch noticing this and looking to streamline your codebase. Let's break down several practical approaches to cut down that boilerplate:

1. Build a Factory Function for Reusable Ducks Modules

You can create a generic factory function that generates the repetitive action creators, reducers, and selectors for you, based on inputs like a feature prefix, API request handlers, and initial state. This way, you don't have to rewrite the same SET/CLEAN/fetch logic for every module.

Here's an example of how to implement this:

// utils/createCRUDDuck.js
import { fetchApi } from '../api';
import { showNotification } from '../notifications';

export const createCRUDDuck = (typePrefix, apiRequests, initialState = null) => {
  // Generate consistent action types
  const types = {
    SET: `${typePrefix}_SET`,
    CLEAN: `${typePrefix}_CLEAN`,
    // Add UPDATE/DELETE types if needed for your use case
  };

  // Reusable action creators
  const setItem = payload => ({ type: types.SET, payload });
  const cleanItem = () => ({ type: types.CLEAN });

  // Generic fetch thunk
  const fetchItem = id => dispatch => {
    return dispatch(fetchApi(apiRequests.get(id)))
      .then(response => dispatch(setItem(response)))
      .catch(error => showNotification(error));
  };

  // Add update/delete actions following the same pattern...

  // Generic reducer
  const reducer = (state = initialState, action) => {
    switch (action.type) {
      case types.SET:
        return action.payload;
      case types.CLEAN:
        return initialState;
      default:
        return state;
    }
  };

  // Consistent selector
  const selectItem = state => state[typePrefix.toLowerCase()];

  return {
    types,
    actions: { setItem, cleanItem, fetchItem },
    reducer,
    selectors: { selectItem }
  };
};

Then use it to generate your feature modules in seconds:

// ducks/user.js
import { createCRUDDuck } from '../utils/createCRUDDuck';
import * as userRequests from '../api/userRequests';

const userDuck = createCRUDDuck('USER', userRequests);
export const { types, actions, userReducer, selectors } = userDuck;

// ducks/transaction.js
import { createCRUDDuck } from '../utils/createCRUDDuck';
import * as transactionRequests from '../api/transactionRequests';

const transactionDuck = createCRUDDuck('TRANSACTION', transactionRequests);
export const { types, actions, transactionReducer, selectors } = transactionDuck;

2. Switch to Redux Toolkit (RTK)

Redux Toolkit is the official recommended approach for writing Redux logic, and it's built specifically to eliminate boilerplate. It integrates redux-thunk by default, uses Immer for mutable-looking (but immutable) reducer updates, and auto-generates action creators/types.

Here's how your user module would look with RTK's createSlice and createAsyncThunk:

// ducks/userSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { fetchApi } from '../api';
import { showNotification } from '../notifications';
import * as userRequests from '../api/userRequests';

// Async thunk for fetching user data
export const fetchUser = createAsyncThunk(
  'user/fetchUser',
  async (userId, { rejectWithValue }) => {
    try {
      const response = await fetchApi(userRequests.get(userId));
      return response;
    } catch (error) {
      showNotification(error);
      return rejectWithValue(error);
    }
  }
);

// Slice with auto-generated actions/reducer
const userSlice = createSlice({
  name: 'user',
  initialState: null,
  reducers: {
    cleanUser: (state) => null, // Immer lets us write direct updates
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchUser.fulfilled, (state, action) => {
        return action.payload; // No need to copy state!
      });
  }
});

export const { cleanUser } = userSlice.actions;
export const userReducer = userSlice.reducer;
export const selectUser = state => state.user;

You can further reduce repetition by extracting shared async logic into a reusable thunk creator, like:

// utils/createFetchThunk.js
import { createAsyncThunk } from '@reduxjs/toolkit';
import { fetchApi } from '../api';
import { showNotification } from '../notifications';

export const createFetchThunk = (actionType, apiCall) => {
  return createAsyncThunk(actionType, async (params, { rejectWithValue }) => {
    try {
      const response = await fetchApi(apiCall(params));
      return response;
    } catch (error) {
      showNotification(error);
      return rejectWithValue(error);
    }
  });
};

Then use it across modules:

// In userSlice.js
export const fetchUser = createFetchThunk('user/fetchUser', userId => userRequests.get(userId));

// In transactionSlice.js
export const fetchTransaction = createFetchThunk('transaction/fetchTransaction', txId => transactionRequests.get(txId));

3. Use RTK Query for API-Centric Modules

If most of your repetition comes from data fetching, caching, and syncing with APIs, RTK Query (built into Redux Toolkit) will eliminate almost all of that boilerplate. It automatically handles loading states, caching, invalidation, and even generates React hooks for data fetching.

Here's a quick example of an API slice that replaces your manual fetch/set/clean logic:

// api/apiSlice.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const apiSlice = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/your-api-base-url' }),
  endpoints: (builder) => ({
    getUser: builder.query({
      query: (userId) => `/users/${userId}`
    }),
    getTransaction: builder.query({
      query: (txId) => `/transactions/${txId}`
    }),
    // Add update/delete mutations here if needed
  })
});

// Auto-generated hooks for components
export const { useGetUserQuery, useGetTransactionQuery } = apiSlice;

In your components, you can use these hooks directly—no need to dispatch actions or write selectors:

import { useGetUserQuery } from '../api/apiSlice';

function UserProfile({ userId }) {
  const { data: user, error, isLoading } = useGetUserQuery(userId);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error!</div>;

  return <div>Welcome, {user.name}!</div>;
}

Final Recommendation

Start with Redux Toolkit—it's the easiest win for reducing boilerplate while staying close to your existing workflow. If you're dealing with a lot of API-driven features, layer in RTK Query to eliminate even more repetitive data fetching logic. Factory functions work great too if you want to stick with the Ducks pattern without switching to RTK entirely.

内容的提问来源于stack exchange,提问作者Arsenowitch

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 06:36:36