使用 Redux Toolkit封装一个模块化的 store

85 阅读2分钟

使用 Redux Toolkit(@reduxjs/toolkit v2.6.1)封装一个模块化的 store,并实现工程化的 Redux 管理,需要考虑模块化、可扩展性和清晰的文件结构。以下是一个完整的实现方案,适用于模块化管理的 Redux 项目。


项目结构

假设你的项目结构如下:

src/
├── store/
│   ├── index.ts           # store 配置入口
│   ├── rootReducer.ts     # 根 reducer 组合
│   ├── hooks.ts           # 自定义 hooks
│   └── modules/           # 模块化切片目录
│       ├── counter/       # 示例模块:counter
│       │   ├── slice.ts   # counter 的 slice
│       │   └── types.ts   # counter 的类型定义
│       └── user/          # 示例模块:user
│           ├── slice.ts   # user 的 slice
│           └── types.ts   # user 的类型定义
├── App.tsx                # 应用入口
└── ...

实现步骤

1. 安装依赖

确保已安装 Redux Toolkit 和 React-Redux:

pnpm install @reduxjs/toolkit@2.6.1 react-redux

2. 创建模块化切片(以 counter 和 user 为例)

counter 模块:src/store/modules/counter/slice.ts

import { createSlice, PayloadAction } from '@reduxjs/toolkit';

// 状态类型
export interface CounterState {
  value: number;
}

const initialState: CounterState = {
  value: 0,
};

/**
 * Counter 模块的 Redux slice
 */
const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload;
    },
  },
});

// 导出 actions 和 reducer
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;

user 模块:src/store/modules/user/slice.ts

import { createSlice, PayloadAction } from '@reduxjs/toolkit';

// 状态类型
export interface UserState {
  name: string;
  isLoggedIn: boolean;
}

const initialState: UserState = {
  name: '',
  isLoggedIn: false,
};

/**
 * User 模块的 Redux slice
 */
const userSlice = createSlice({
  name: 'user',
  initialState,
  reducers: {
    login: (state, action: PayloadAction<string>) => {
      state.name = action.payload;
      state.isLoggedIn = true;
    },
    logout: (state) => {
      state.name = '';
      state.isLoggedIn = false;
    },
  },
});

// 导出 actions 和 reducer
export const { login, logout } = userSlice.actions;
export default userSlice.reducer;

3. 组合根 Reducer(src/store/rootReducer.ts

将所有模块的 reducer 组合成一个根 reducer:

import { combineReducers } from '@reduxjs/toolkit';
import counterReducer from './modules/counter/slice';
import userReducer from './modules/user/slice';

const rootReducer = combineReducers({
  counter: counterReducer,
  user: userReducer,
});

export type RootState = ReturnType<typeof rootReducer>;
export default rootReducer;
  • RootState 类型用于后续类型推导,确保类型安全。

    • [3.1]
      若希望其中的 combineReducers 可以批量导入,可以使用如下方案
      • 方案一:仅适用于 webpack 项目, combineReducers 批量导入
      • 【Next.js 默认使用 Webpack 作为打包工具(截至 2025 年 3 月 28 日,Turbopack 仍为实验性),若Next.js项目中指定使用Turbopack的话,目前了解下来Turbopack暂时并没有暴露出任何批量导入方法,也就是说如果你给项目指定使用了Turbopack打包,就只能用上面的逐个手动导入的方式了】
import { combineReducers } from '@reduxjs/toolkit';

const context = require.context('./modules/', true, /slice.ts$/);
const reducers = {};

context.keys()
  .filter((key) => key.split('/').length === 4)
  .forEach((key) => {
    const reducer = context(key).default;
    const keyName = key.split('/')[2];
    reducers[keyName] = reducer;
  });

const rootReducer = combineReducers(reducers);

export type RootState = ReturnType<typeof rootReducer>;
export default rootReducer;
      • 方案二:仅适用于 vite 项目, combineReducers 批量导入
import { combineReducers } from '@reduxjs/toolkit';

// 批量导入 ./modules 下所有 slice.ts 文件
const modules = import.meta.glob('./modules/*/slice.ts', { eager: true });

const reducers = Object.keys(modules).reduce((acc, path) => {
  // 从路径中提取文件夹名称作为键,例如 './modules/counter/slice.ts' -> 'counter'
  const key = path.split('/')[2];
  // 获取默认导出的 reducer
  const reducer = modules[path].default;
  acc[key] = reducer;
  return acc;
}, {});

const rootReducer = combineReducers(reducers);

export type RootState = ReturnType<typeof rootReducer>;
export default rootReducer;
      • 方案三:仅适用于 NextJs 项目, combineReducers 批量导入
import { combineReducers } from '@reduxjs/toolkit';

// 批量导入 ./modules 下所有 slice.ts 文件
const modules = import.meta.glob('./modules/*/slice.ts', { eager: true });

const reducers = Object.keys(modules).reduce((acc, path) => {
  // 从路径中提取文件夹名称作为键,例如 './modules/counter/slice.ts' -> 'counter'
  const key = path.split('/')[2];
  // 获取默认导出的 reducer
  const reducer = modules[path].default;
  acc[key] = reducer;
  return acc;
}, {});

const rootReducer = combineReducers(reducers);

export type RootState = ReturnType<typeof rootReducer>;
export default rootReducer;

4. 配置 Store(src/store/index.ts

使用 configureStore 创建一个模块化的 store:

import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './rootReducer';

const store = configureStore({
  reducer: rootReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: false, // 可选:关闭序列化检查(如果需要非序列化状态)
    }),
  devTools: process.env.NODE_ENV !== 'production', // 开发环境启用 Redux DevTools
});

export default store;

// 导出类型用于 hooks
export type AppDispatch = typeof store.dispatch;
  • configureStore 自带默认中间件(如 redux-thunk),并支持 Redux DevTools。
  • AppDispatch 类型用于类型安全的 dispatch

5. 创建自定义 Hooks(src/store/hooks.ts

为了方便在 React 组件中使用类型安全的 Redux,创建自定义 hooks:

import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import { RootState, AppDispatch } from '../store';

// 类型安全的 useSelector
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

// 类型安全的 useDispatch
export const useAppDispatch = () => useDispatch<AppDispatch>();

6. 在应用中集成(src/App.tsx

将 store 提供给 React 应用,并使用模块化的 Redux:

import React from 'react';
import { Provider } from 'react-redux';
import store from './store';
import Counter from './Counter';
import User from './User';

const App: React.FC = () => {
  return (
    <Provider store={store}>
      <div>
        <h1>Redux Toolkit Modular Demo</h1>
        <Counter />
        <User />
      </div>
    </Provider>
  );
};

export default App;

示例组件:src/Counter.tsx

import React from 'react';
import { useAppSelector, useAppDispatch } from './store/hooks';
import { increment, decrement, incrementByAmount } from './store/modules/counter/slice';

const Counter: React.FC = () => {
  const count = useAppSelector((state) => state.counter.value);
  const dispatch = useAppDispatch();

  return (
    <div>
      <h2>Counter: {count}</h2>
      <button onClick={() => dispatch(increment())}>Increment</button>
      <button onClick={() => dispatch(decrement())}>Decrement</button>
      <button onClick={() => dispatch(incrementByAmount(5))}>Add 5</button>
    </div>
  );
};

export default Counter;

示例组件:src/User.tsx

import React from 'react';
import { useAppSelector, useAppDispatch } from './store/hooks';
import { login, logout } from './store/modules/user/slice';

const User: React.FC = () => {
  const { name, isLoggedIn } = useAppSelector((state) => state.user);
  const dispatch = useAppDispatch();

  return (
    <div>
      <h2>User: {isLoggedIn ? name : 'Not logged in'}</h2>
      <button onClick={() => dispatch(login('Alice'))}>Login as Alice</button>
      <button onClick={() => dispatch(logout())}>Logout</button>
    </div>
  );
};

export default User;

特点与优势

  1. 模块化管理

    • 每个功能模块(如 counteruser)都有独立的 slice 文件,包含状态、reducer 和 actions。
    • 通过 rootReducer 组合所有模块,保持清晰的层次结构。
  2. 类型安全

    • 使用 TypeScript 定义状态和 dispatch 类型,避免类型错误。
    • 自定义 useAppSelectoruseAppDispatch 提供类型支持。
  3. Redux Toolkit v2.6.1 特性

    • 使用 createSlice 简化 reducer 和 action 创建。
    • configureStore 自带默认中间件和 DevTools 支持。
    • 支持异步逻辑(可通过 createAsyncThunk 扩展)。
  4. 可扩展性

    • 添加新模块时,只需创建新的 slice 并在 rootReducer 中注册即可。

扩展建议

  • 异步逻辑:使用 createAsyncThunk 处理 API 请求。
    import { createAsyncThunk } from '@reduxjs/toolkit';
    export const fetchUser = createAsyncThunk('user/fetch', async (id: string) => {
      const response = await fetch(`/api/user/${id}`);
      return response.json();
    });
    
  • 中间件:根据需要添加自定义中间件(如日志中间件)。
  • RTK Query:如果需要数据获取和缓存,可以集成 Redux Toolkit Query。

这个封装方案已经完全模块化,适用于中小型到大型项目。你可以根据具体需求调整目录结构或添加更多功能。如果有其他问题或需要优化某个部分,请告诉我!