一、背景
在构建 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 状态和操作
在组件中通过 useSelector
和 useDispatch
使用 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 的 lazy
和 Suspense
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 模块的方式非常适合大型项目,尤其是模块众多但同时使用的模块有限的场景。