Bootstrap

HOW - React 状态模块化管理和按需加载(一) - react-redux

一、背景

在构建 React 项目时,必不可少的就是一些全局或者多组件共享数据的管理。官方推荐使用 react-redux.

但现在还流行另外一个框架,jotai.

我们今天就来学习一下如何基于 react-redux进行状态模块化管理和按需加载,包括为什么且在什么场景下可以引入 jotai 使用。

二、react-redux

关于 react-redux 基本使用就不过多介绍了。

模块化管理

在 React + Redux 项目中,将 Redux 状态和逻辑分模块管理是一种常见的实践,可以提高代码的可维护性和可扩展性。以下是分模块管理 Redux 的常用方法和步骤:

1. 模块化文件结构

一种常见的项目结构是将每个功能模块的状态管理放在单独的文件夹中。
例如,一个项目包含用户管理和商品管理模块,可以按如下结构组织:

src/
├── store/          // Redux 全局 store
│   ├── index.ts    // 配置 store
│   ├── rootReducer.ts // 合并 reducers
├── features/
│   ├── user/       // 用户模块
│   │   ├── userSlice.ts
│   │   ├── userActions.ts
│   │   ├── userSelectors.ts
│   ├── product/    // 商品模块
│   │   ├── productSlice.ts
│   │   ├── productActions.ts
│   │   ├── productSelectors.ts

2. 使用 Redux Toolkit 的 Slice

Redux Toolkit 提供了 createSlice 方法,可以更轻松地管理每个模块的状态和操作。

例子:用户模块 (userSlice)
// src/features/user/userSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface UserState {
  userInfo: { id: string; name: string } | null;
  isLoading: boolean;
}

const initialState: UserState = {
  userInfo: null,
  isLoading: false,
};

const userSlice = createSlice({
  name: 'user',
  initialState,
  reducers: {
    setUserInfo(state, action: PayloadAction<{ id: string; name: string }>) {
      state.userInfo = action.payload;
    },
    clearUserInfo(state) {
      state.userInfo = null;
    },
    setLoading(state, action: PayloadAction<boolean>) {
      state.isLoading = action.payload;
    },
  },
});

export const { setUserInfo, clearUserInfo, setLoading } = userSlice.actions;
export default userSlice.reducer;
例子:商品模块 (productSlice)
// src/features/product/productSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface Product {
  id: string;
  name: string;
  price: number;
}

interface ProductState {
  productList: Product[];
  isLoading: boolean;
}

const initialState: ProductState = {
  productList: [],
  isLoading: false,
};

const productSlice = createSlice({
  name: 'product',
  initialState,
  reducers: {
    setProducts(state, action: PayloadAction<Product[]>) {
      state.productList = action.payload;
    },
    addProduct(state, action: PayloadAction<Product>) {
      state.productList.push(action.payload);
    },
    setLoading(state, action: PayloadAction<boolean>) {
      state.isLoading = action.payload;
    },
  },
});

export const { setProducts, addProduct, setLoading } = productSlice.actions;
export default productSlice.reducer;

3. 合并 Reducers

使用 Redux Toolkit 的 combineReducers 将模块化的 reducer 合并。

// src/store/rootReducer.ts
import { combineReducers } from '@reduxjs/toolkit';
import userReducer from '../features/user/userSlice';
import productReducer from '../features/product/productSlice';

const rootReducer = combineReducers({
  user: userReducer,
  product: productReducer,
});

export default rootReducer;

4. 配置 Store

在 Redux Toolkit 中,通过 configureStore 来配置全局 Store。

// src/store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './rootReducer';

const store = configureStore({
  reducer: rootReducer,
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

export default store;

5. 使用 Redux 状态和操作

在组件中通过 useSelectoruseDispatch 使用 Redux 状态和操作。

例子:获取用户信息
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from '../../store';
import { setUserInfo } from '../../features/user/userSlice';

const UserProfile: React.FC = () => {
  const userInfo = useSelector((state: RootState) => state.user.userInfo);
  const dispatch = useDispatch();

  const handleLogin = () => {
    dispatch(setUserInfo({ id: '1', name: 'John Doe' }));
  };

  return (
    <div>
      {userInfo ? (
        <p>Welcome, {userInfo.name}</p>
      ) : (
        <p>Please log in</p>
      )}
      <button onClick={handleLogin}>Log In</button>
    </div>
  );
};

export default UserProfile;

6. 拓展

  • 异步操作:使用 Redux Toolkit 的 createAsyncThunk 管理异步逻辑。
  • 代码分割:结合动态加载模块,按需加载 Redux 状态和逻辑。
  • 测试:为每个模块单独编写单元测试,提高代码稳定性。

这种分模块的方式可以帮助你清晰地组织 Redux 状态,增强项目的可维护性。

按需加载

在某些场景下,只需要多个模块之间共享一份数据,即这份数据并不是 global 全局的。

则可以通过 按需加载 Redux 模块 来实现内存中只加载需要的模块,从而优化性能和内存占用。

这通常结合 动态模块加载 和 Redux 的 replaceReducer 功能来实现。

以下是如何实现按需加载 Redux 模块的完整方案:

1. 动态模块加载的核心逻辑

Redux 支持使用 store.replaceReducer 替换当前的根 reducer,这样就可以在需要时动态添加新的模块。

2. 实现动态加载 Reducer

修改 Store 配置

首先,创建一个支持动态加载的 Store。

// src/store/index.ts
import { configureStore, Reducer, CombinedState } from '@reduxjs/toolkit';
import { combineReducers } from 'redux';

type AsyncReducers = {
  [key: string]: Reducer;
};

const createRootReducer = (asyncReducers: AsyncReducers) =>
  combineReducers({
    // 这里可以放一些全局的静态 reducer
    ...asyncReducers,
  });

const store = configureStore({
  reducer: createRootReducer({}),
});

type RootState = ReturnType<typeof store.getState>;
type AppDispatch = typeof store.dispatch;

store.asyncReducers = {} as AsyncReducers;

store.injectReducer = (key: string, reducer: Reducer) => {
  if (!store.asyncReducers[key]) {
    store.asyncReducers[key] = reducer;
    store.replaceReducer(createRootReducer(store.asyncReducers));
  }
};

export { RootState, AppDispatch };
export default store;

说明:

  • store.asyncReducers 用于存储动态加载的 reducers。
  • store.injectReducer 是动态注入 reducer 的方法。

3. 动态注入模块的 Reducer

在需要时加载模块并动态注入其 reducer。

示例:动态加载 user 模块
// src/features/user/index.ts
import userReducer from './userSlice';
import store from '../../store';

store.injectReducer('user', userReducer);
示例:动态加载 product 模块
// src/features/product/index.ts
import productReducer from './productSlice';
import store from '../../store';

store.injectReducer('product', productReducer);

4. 组件中按需加载模块

在组件中使用模块时,可以按需加载 reducer。

示例:加载 user 模块
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from '../../store';
import { setUserInfo } from './userSlice';

// 动态加载 user 模块
import('./index');

const UserProfile: React.FC = () => {
  const userInfo = useSelector((state: RootState) => state.user?.userInfo);
  const dispatch = useDispatch();

  useEffect(() => {
    if (!userInfo) {
      dispatch(setUserInfo({ id: '1', name: 'John Doe' }));
    }
  }, [dispatch, userInfo]);

  return <div>Welcome, {userInfo?.name || 'Guest'}</div>;
};

export default UserProfile;

说明

  • import('./index') 动态加载 user 模块并注入 reducer。
  • 使用时无需提前加载其他模块,减少内存占用。

5. 结合代码分割

使用 Webpack 或 Vite 的代码分割功能,将每个模块单独打包,按需加载。

示例:使用 React 的 lazySuspense
import React, { Suspense } from 'react';

const UserProfile = React.lazy(() => import('./features/user/UserProfile'));

const App: React.FC = () => {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <UserProfile />
    </Suspense>
  );
};

export default App;

效果

  • UserProfile 页面访问时,动态加载 user 模块。
  • 内存中仅保留使用过的模块(如 user),未使用的模块(如 product)不会加载。

6. 清除不需要的模块

如果模块加载后不再需要,可以从内存中移除 reducer。

实现移除 Reducer 的方法
store.removeReducer = (key: string) => {
  if (store.asyncReducers[key]) {
    delete store.asyncReducers[key];
    store.replaceReducer(createRootReducer(store.asyncReducers));
  }
};
示例:移除 user 模块
store.removeReducer('user');

7. 优势

  • 减少内存占用:未使用的模块不会加载到内存中。
  • 优化首屏性能:只加载当前页面需要的模块。
  • 代码分离:结合动态加载和代码分割,实现更清晰的模块管理。

这种按需加载 Redux 模块的方式非常适合大型项目,尤其是模块众多但同时使用的模块有限的场景。

;