
在现代化前端开发中,如何优雅地管理状态、组织代码并提升性能,是每个团队都会面临的挑战。本文结合 React Hooks、Redux Toolkit 和 Ant Design,从项目架构到性能优化,分享一套经过生产验证的实践方案,帮助你在中大型后台项目中游刃有余。
Hooks 自 16.8 引入后,彻底改变了 React 组件的编写方式。它让我们在不编写 Class 的情况下使用 state 和生命周期,更重要的是,自定义 Hooks 让逻辑复用变得前所未有的简单。相比 HOC 或 render props,Hooks 更直观、嵌套更少,也更容易进行类型推断。
Redux 本身因样板代码过多而备受诟病,而 Redux Toolkit(RTK)正是为了解决这一问题而生。它集成了 immer、createSlice、createAsyncThunk 等工具,极大减少了 reducer 和 action 的冗余代码,同时默认配置了 Redux DevTools 和 Thunk 中间件,开箱即用。
Antd 提供了丰富的组件和设计语言,能够快速搭建专业后台界面。其 Form、Table、Modal 等组件与 Redux 配合使用时,需要注意数据流的管理,这也是本文的重点之一。
良好的项目结构是代码可维护性的基石。我们采用 功能模块划分 + 分层设计 的方式:
src/
├── api/ # API 请求定义
│ └── user.js
├── assets/ # 静态资源
├── components/ # 通用 UI 组件(无状态或仅依赖 props)
├── hooks/ # 自定义 Hooks
│ ├── useAuth.js
│ └── useTable.js
├── pages/ # 页面级组件(路由入口)
│ ├── Dashboard/
│ └── UserManage/
├── store/ # Redux 相关
│ ├── index.js # 配置 store
│ ├── rootReducer.js
│ └── slices/ # 按模块划分 slice
│ ├── userSlice.js
│ └── appSlice.js
├── utils/ # 工具函数
├── constants/ # 常量
└── App.js这种结构使得每个模块(如用户管理)相关的 slice、API、页面组件放在一起,便于迭代和多人协作。
createSlice 减少样板代码传统 Redux 需要手写 action types、action creators 和 reducers,而 createSlice 自动生成它们:
// store/slices/userSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { fetchUserList } from '@/api/user';
// 异步 thunk
export const getUsers = createAsyncThunk(
'user/getUsers',
async (params, { rejectWithValue }) => {
try {
const response = await fetchUserList(params);
return response.data;
} catch (err) {
return rejectWithValue(err.response.data);
}
}
);
const userSlice = createSlice({
name: 'user',
initialState: {
list: [],
loading: false,
error: null,
pagination: { current: 1, pageSize: 10, total: 0 },
},
reducers: {
// 同步 reducer,例如重置搜索条件
resetSearch: (state) => {
state.pagination.current = 1;
state.pagination.total = 0;
},
},
extraReducers: (builder) => {
builder
.addCase(getUsers.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(getUsers.fulfilled, (state, action) => {
state.loading = false;
state.list = action.payload.records;
state.pagination.total = action.payload.total;
})
.addCase(getUsers.rejected, (state, action) => {
state.loading = false;
state.error = action.payload || '请求失败';
});
},
});
export const { resetSearch } = userSlice.actions;
export default userSlice.reducer;如果你的应用需要管理大量实体(如用户、订单),且需要按 ID 快速查找、增删改,可以使用 createEntityAdapter,它能生成预置的 reducer 和 selector,极大简化 CURD 操作。
import { createEntityAdapter } from '@reduxjs/toolkit';
const userAdapter = createEntityAdapter({
selectId: (user) => user.id,
sortComparer: (a, b) => a.name.localeCompare(b.name),
});
const initialState = userAdapter.getInitialState({
loading: false,
error: null,
});
// 在 extraReducers 中调用 userAdapter 的方法
builder.addCase(getUsers.fulfilled, (state, action) => {
userAdapter.setAll(state, action.payload);
});useSelector 优化性能useSelector 默认使用 === 引用比较,如果返回的是新对象(如 state => state.user),即使数据未变,也会导致组件重新渲染。建议配合 reselect 或使用 shallowEqual:
import { useSelector, shallowEqual } from 'react-redux';
// 只选取需要的数据,避免整个对象
const { list, loading } = useSelector(
(state) => ({
list: state.user.list,
loading: state.user.loading,
}),
shallowEqual // 浅比较
);更优方案:使用 createSelector 缓存派生数据(例如过滤、排序后的列表),避免每次计算。
useAppDispatch 和 useAppSelector(TypeScript 友好)在 TypeScript 项目中,我们通常封装这两个 hooks 以获得正确的类型推断:
// store/index.ts
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
import { AppDispatch, RootState } from './store';
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;将页面中的状态管理、数据请求、表单逻辑抽离到自定义 hooks 中,让组件保持干净。
示例:表格数据管理 Hook
// hooks/useTable.js
import { useCallback, useEffect } from 'react';
import { useAppDispatch, useAppSelector } from '@/store';
import { getUsers, resetSearch } from '@/store/slices/userSlice';
export const useUserTable = () => {
const dispatch = useAppDispatch();
const { list, loading, pagination } = useAppSelector(
(state) => state.user,
shallowEqual
);
const fetchData = useCallback((params = {}) => {
dispatch(getUsers({ ...pagination, ...params }));
}, [dispatch, pagination]);
const handleTableChange = (pagination) => {
dispatch(getUsers({ page: pagination.current, pageSize: pagination.pageSize }));
};
const reset = useCallback(() => {
dispatch(resetSearch());
}, [dispatch]);
useEffect(() => {
fetchData();
}, [fetchData]);
return { list, loading, pagination, handleTableChange, reset, fetchData };
};在组件中使用:
const UserList = () => {
const { list, loading, pagination, handleTableChange } = useUserTable();
return (
<Table
dataSource={list}
loading={loading}
pagination={pagination}
onChange={handleTableChange}
// columns...
/>
);
};Antd Form 默认是受控的,其内部状态由 Form 实例管理。不建议将表单数据全部存入 Redux,因为这样会导致频繁的全局更新,影响性能。更好的做法是:
Form.useForm 管理表单实例,仅在提交时触发 Redux action。onValuesChange 时 debounce 更新 Redux,但注意不要过于频繁。推荐模式:
const SearchForm = () => {
const [form] = Form.useForm();
const dispatch = useAppDispatch();
const onFinish = (values) => {
dispatch(updateSearchParams(values)); // 更新 Redux 中的搜索条件
dispatch(getUsers(values)); // 触发数据请求
};
return (
<Form form={form} onFinish={onFinish}>
{/* Form items */}
<Button htmlType="submit">搜索</Button>
</Form>
);
};对于全局提示(message/notification)和弹窗,建议通过 Redux 管理其显示状态,以便在异步 thunk 中控制。
appSlice:
const appSlice = createSlice({
name: 'app',
initialState: {
modal: { visible: false, type: 'confirm', data: null },
notification: null,
},
reducers: {
showModal: (state, action) => {
state.modal = { ...state.modal, ...action.payload, visible: true };
},
hideModal: (state) => {
state.modal.visible = false;
},
},
});在 thunk 中处理错误提示:
export const getUsers = createAsyncThunk(
'user/getUsers',
async (params, { dispatch, rejectWithValue }) => {
try {
const res = await api.getUsers(params);
return res.data;
} catch (error) {
// 显示全局错误通知
dispatch(showNotification({ type: 'error', message: error.message }));
return rejectWithValue(error.message);
}
}
);建议将分页参数放在 Redux 中,这样当路由切换再返回时,可以保留表格状态。使用上面 useTable 的思路,在 handleTableChange 中 dispatch 更新并重新请求数据。
arePropsEqual 自定义比较。reselect 创建记忆化选择器,避免在每次 state 更新时重新计算派生数据。useSelector 只选取组件真正需要的最小数据。batch)。当 Table 数据量大时,考虑使用 Antd 的虚拟滚动(virtual 属性)或分页。同时,为每行设置唯一的 key,避免使用索引。
createAsyncThunk 的进阶用法getUsers({ page: 1 })rejectWithValue 返回自定义错误信息。getState 检查当前状态,避免重复请求(例如正在 loading 时跳过)。export const getUsers = createAsyncThunk(
'user/getUsers',
async (params, { getState, rejectWithValue }) => {
const { user } = getState();
if (user.loading) return; // 防重复请求
// ...
}
);虽然 React Hooks 不能直接使用 Error Boundary(需用 Class 组件),但可以为整个应用包裹一个 Error Boundary 组件,捕获渲染错误并显示降级 UI。同时,在 thunk 中捕获请求错误,通过 Redux 管理错误状态,并展示给用户。
使用 @testing-library/react 和 redux-mock-store 测试异步 thunk:
import { getUsers } from './userSlice';
import mockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
const middlewares = [thunk];
const store = mockStore(middlewares)({});
test('getUsers fulfilled', async () => {
await store.dispatch(getUsers({ page: 1 }));
const actions = store.getActions();
expect(actions[0].type).toBe(getUsers.pending.type);
// 模拟 API 成功...
});测试页面组件与 Redux 的交互,使用 Provider 包裹测试组件,并 mock API。
本文从项目架构、状态管理、Hooks 封装、UI 联动、性能优化和测试等方面,系统介绍了 React + Redux Toolkit + Antd 的实战经验。这套方案已在多个中大型项目中落地,显著提升了开发效率和代码质量。
当然,技术选型并非一成不变。随着 React Server Components、Next.js App Router 的兴起,部分场景可能会转向服务端状态管理(如 React Query、SWR)与客户端状态(如 Jotai、Zustand)的混合模式。但无论如何,扎实的状态管理思维和性能优化意识始终是前端工程师的核心竞争力。
希望本文能为你带来启发,如果你有更好的实践,欢迎在评论区交流。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。