一套完整的状态管理、UI 组件库与函数式编程实践方案,助力中大型项目高效交付。
在当今前端工程化浪潮中,React 凭借其声明式视图和灵活生态稳居主流框架前列。然而,真实企业级项目不仅需要“能跑”,更要可维护、可扩展、高性能。本文基于实际项目经验,深度整合 Redux Toolkit(状态管理)、Ant Design 4.x(企业级 UI 库)与 React Hooks(逻辑复用),从零搭建一个具备登录认证、数据看板、动态表单等典型场景的脚手架。全文代码均经过生产环境验证,拒绝“玩具示例”,直击工程痛点。
技术栈 | 版本 | 用途 |
|---|---|---|
React | 18.2.0 | 视图层(Concurrent 模式) |
React Router DOM | 6.14.0 | 路由(嵌套路由 + 权限控制) |
Redux Toolkit (RTK) | 1.9.5 | 全局状态管理(含 Immer、Thunk) |
React-Redux | 8.1.1 | React 绑定钩子(useSelector/useDispatch) |
Ant Design | 5.9.0 | 组件库(支持 CSS-in-JS 动态主题) |
Axios | 1.4.0 | HTTP 拦截器 + 请求取消 |
TypeScript | 5.1.6 | 类型安全(本文示例以 TS 书写) |
核心思想:RTK 替代传统 Redux 样板代码,Hooks 替代 Class 组件,Antd 提供开箱即用的设计系统。
src/
├── api/ # 接口定义(按模块)
│ ├── user.ts
│ └── dashboard.ts
├── app/ # 全局配置
│ ├── store.ts # Redux store 配置
│ └── rootReducer.ts # 组合 reducer
├── features/ # 功能模块(DDD 风格)
│ ├── auth/
│ │ ├── authSlice.ts
│ │ ├── AuthGuard.tsx
│ │ └── LoginPage.tsx
│ ├── dashboard/
│ │ ├── dashboardSlice.ts
│ │ └── DashboardPage.tsx
│ └── common/ # 通用组件(Layout、Table 封装)
├── hooks/ # 自定义 Hooks(全局复用)
│ ├── useRequest.ts
│ └── useAuth.ts
├── utils/ # 工具函数(axios 实例、token 处理)
└── styles/ # 全局样式(Antd 主题变量)// app/store.ts
import { configureStore } from '@reduxjs/toolkit';
import logger from 'redux-logger';
import rootReducer from './rootReducer';
export const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: ['auth/login/fulfilled'], // 忽略非序列化警告
},
}).concat(import.meta.env.DEV ? logger : []),
devTools: process.env.NODE_ENV !== 'production',
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;以 authSlice 为例,展示登录、登出、自动刷新 token 的完整闭环:
// features/auth/authSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import axios, { AxiosError } from 'axios';
import { setToken, removeToken, getToken } from '@/utils/token';
interface AuthState {
user: User | null;
token: string | null;
loading: 'idle' | 'pending' | 'succeeded' | 'failed';
error: string | null;
}
const initialState: AuthState = {
user: null,
token: getToken() || null,
loading: 'idle',
error: null,
};
// 异步 Thunk:登录
export const login = createAsyncThunk(
'auth/login',
async (credentials: { username: string; password: string }, { rejectWithValue }) => {
try {
const response = await axios.post('/api/auth/login', credentials);
const { accessToken, user } = response.data.data;
setToken(accessToken); // 写入 localStorage
return { token: accessToken, user };
} catch (err) {
const error = err as AxiosError<{ message: string }>;
return rejectWithValue(error.response?.data?.message || '登录失败');
}
}
);
// 异步 Thunk:获取当前用户(用于刷新/恢复)
export const fetchCurrentUser = createAsyncThunk(
'auth/fetchUser',
async (_, { getState, rejectWithValue }) => {
const state = getState() as RootState;
const token = state.auth.token;
if (!token) return rejectWithValue('无 token');
try {
const res = await axios.get('/api/auth/me', {
headers: { Authorization: `Bearer ${token}` },
});
return res.data.data;
} catch {
removeToken();
return rejectWithValue('用户信息过期');
}
}
);
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
logout: (state) => {
state.user = null;
state.token = null;
removeToken();
},
resetError: (state) => {
state.error = null;
},
},
extraReducers: (builder) => {
builder
.addCase(login.pending, (state) => {
state.loading = 'pending';
state.error = null;
})
.addCase(login.fulfilled, (state, action) => {
state.loading = 'succeeded';
state.token = action.payload.token;
state.user = action.payload.user;
})
.addCase(login.rejected, (state, action) => {
state.loading = 'failed';
state.error = action.payload as string;
})
.addCase(fetchCurrentUser.fulfilled, (state, action) => {
state.user = action.payload;
})
.addCase(fetchCurrentUser.rejected, (state) => {
state.user = null;
state.token = null;
removeToken();
});
},
});
export const { logout, resetError } = authSlice.actions;
export default authSlice.reducer;// app/hooks.ts
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
import type { RootState, AppDispatch } from './store';
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;// App.tsx
import { ConfigProvider, theme } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import { useAppSelector } from '@/app/hooks';
function App() {
const { darkMode } = useAppSelector((state) => state.settings);
return (
<ConfigProvider
locale={zhCN}
theme={{
algorithm: darkMode ? theme.darkAlgorithm : theme.defaultAlgorithm,
token: {
colorPrimary: '#1890ff',
borderRadius: 4,
},
components: {
Table: {
headerBg: '#fafafa',
},
},
}}
>
<RouterProvider router={router} />
</ConfigProvider>
);
}企业后台常遇到“根据选择类型动态渲染字段”的场景,利用 Antd Form 的 shouldUpdate 和 Hooks 实现:
// features/product/ProductForm.tsx
import { Form, Input, Select, DatePicker, Button } from 'antd';
import { useForm, FormInstance } from 'antd/es/form/Form';
import { useEffect } from 'react';
interface ProductFormProps {
initialValues?: any;
onSubmit: (values: any) => void;
}
export const ProductForm: React.FC<ProductFormProps> = ({ initialValues, onSubmit }) => {
const [form] = useForm();
// 监听类型变化
const typeValue = Form.useWatch('type', form);
useEffect(() => {
// 当类型改变时重置关联字段
if (typeValue === 'digital') {
form.setFieldsValue({ warranty: undefined });
}
}, [typeValue, form]);
const handleFinish = (values: any) => {
// 提交前格式化
const payload = {
...values,
price: values.price * 100, // 分转元
};
onSubmit(payload);
};
return (
<Form
form={form}
layout="vertical"
initialValues={initialValues}
onFinish={handleFinish}
scrollToFirstError
>
<Form.Item
name="name"
label="产品名称"
rules={[{ required: true, message: '请输入名称' }]}
>
<Input maxLength={20} />
</Form.Item>
<Form.Item
name="type"
label="产品类型"
rules={[{ required: true }]}
>
<Select>
<Select.Option value="physical">实物</Select.Option>
<Select.Option value="digital">虚拟商品</Select.Option>
</Select>
</Form.Item>
{/* 条件渲染:虚拟商品展示有效期 */}
{typeValue === 'digital' && (
<Form.Item
name="expireAt"
label="有效期"
rules={[{ required: true, type: 'object' }]}
>
<DatePicker showTime />
</Form.Item>
)}
<Form.Item
name="price"
label="价格(元)"
rules={[
{ required: true },
{ type: 'number', min: 0, message: '价格不能为负' },
]}
>
<Input type="number" step="0.01" prefix="¥" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">提交</Button>
</Form.Item>
</Form>
);
};useRequest 与 useAuth// hooks/useRequest.ts
import { useState, useEffect, useRef, useCallback } from 'react';
import { AxiosRequestConfig, CancelTokenSource } from 'axios';
import axiosInstance from '@/utils/axios';
interface UseRequestOptions<T> {
manual?: boolean; // 是否手动触发
debounceWait?: number; // 防抖延迟(ms)
retryCount?: number; // 失败重试次数
onSuccess?: (data: T) => void;
onError?: (err: Error) => void;
}
export function useRequest<T = any>(
config: AxiosRequestConfig,
options: UseRequestOptions<T> = {}
) {
const { manual = false, debounceWait = 0, retryCount = 0 } = options;
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const cancelSource = useRef<CancelTokenSource | null>(null);
const timerRef = useRef<NodeJS.Timeout | null>(null);
const retries = useRef(0);
const run = useCallback(async (params?: any) => {
if (cancelSource.current) {
cancelSource.current.cancel('请求被取消');
}
cancelSource.current = axiosInstance.CancelToken.source();
setLoading(true);
setError(null);
try {
const response = await axiosInstance.request({
...config,
params: { ...config.params, ...params },
cancelToken: cancelSource.current.token,
});
setData(response.data);
options.onSuccess?.(response.data);
retries.current = 0;
return response.data;
} catch (err: any) {
if (err.message === '请求被取消') return;
setError(err);
options.onError?.(err);
// 重试逻辑
if (retryCount > 0 && retries.current < retryCount) {
retries.current += 1;
setTimeout(() => run(params), 1000 * retries.current);
}
} finally {
setLoading(false);
}
}, [config, options, retryCount]);
// 防抖处理
const debouncedRun = useCallback((params?: any) => {
if (debounceWait > 0) {
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => run(params), debounceWait);
} else {
run(params);
}
}, [run, debounceWait]);
useEffect(() => {
if (!manual) {
debouncedRun();
}
return () => {
if (cancelSource.current) cancelSource.current.cancel('组件卸载');
if (timerRef.current) clearTimeout(timerRef.current);
};
}, [manual, debouncedRun]);
return { data, loading, error, run: debouncedRun, cancel: () => cancelSource.current?.cancel() };
}// hooks/useAuth.ts
import { useAppSelector, useAppDispatch } from '@/app/hooks';
import { useNavigate } from 'react-router-dom';
import { logout } from '@/features/auth/authSlice';
export const useAuth = () => {
const { user, token } = useAppSelector((state) => state.auth);
const dispatch = useAppDispatch();
const navigate = useNavigate();
const checkPermission = (permission: string): boolean => {
if (!user) return false;
return user.permissions?.includes(permission) ?? false;
};
const logoutUser = () => {
dispatch(logout());
navigate('/login', { replace: true });
};
return { user, token, isAuthenticated: !!token, checkPermission, logoutUser };
};使用 React Router v6 的 loader 和 lazy 实现按需加载 + 权限拦截:
// router/index.tsx
import { createBrowserRouter, Navigate } from 'react-router-dom';
import { lazy, Suspense } from 'react';
import { Spin } from 'antd';
import AuthGuard from '@/features/auth/AuthGuard';
import Layout from '@/components/Layout';
const Dashboard = lazy(() => import('@/features/dashboard/DashboardPage'));
const ProductList = lazy(() => import('@/features/product/ProductList'));
const Login = lazy(() => import('@/features/auth/LoginPage'));
const withSuspense = (Component: React.ComponentType) => (
<Suspense fallback={<Spin size="large" style={{ margin: '20% 50%' }} />}>
<Component />
</Suspense>
);
export const router = createBrowserRouter([
{
path: '/login',
element: withSuspense(Login),
},
{
path: '/',
element: <AuthGuard />, // 路由守卫包裹 Layout
children: [
{
index: true,
element: <Navigate to="/dashboard" />,
},
{
path: 'dashboard',
element: withSuspense(Dashboard),
loader: async () => {
// 预加载数据(如统计数据)
return fetchDashboardStats();
},
},
{
path: 'products',
element: withSuspense(ProductList),
// 路由级权限(可配合 loader 校验)
loader: async ({ request }) => {
const user = await getUserFromStore();
if (!user?.permissions.includes('product:view')) {
throw redirect('/403');
}
return null;
},
},
],
},
]);AuthGuard 实现(基于 Redux 状态):
// features/auth/AuthGuard.tsx
import { Outlet, Navigate } from 'react-router-dom';
import { useAppSelector } from '@/app/hooks';
import { useEffect } from 'react';
import { fetchCurrentUser } from './authSlice';
export default function AuthGuard() {
const { token, loading } = useAppSelector((state) => state.auth);
const dispatch = useAppDispatch();
useEffect(() => {
if (token) {
dispatch(fetchCurrentUser()); // 恢复用户信息
}
}, [token, dispatch]);
if (!token) {
return <Navigate to="/login" replace />;
}
// 可展示全局 Loading
if (loading === 'pending') {
return <Spin fullscreen />;
}
return <Outlet />;
}// features/dashboard/dashboardSlice.ts
import { createSelector } from '@reduxjs/toolkit';
const selectDashboard = (state: RootState) => state.dashboard;
export const selectFilteredChartData = createSelector(
[selectDashboard, (state, filterType) => filterType],
(dashboard, filterType) => {
// 耗时计算(仅当 data 或 filterType 变化时重新计算)
return dashboard.rawData.filter(item => item.type === filterType);
}
);在组件中使用:
const chartData = useAppSelector((state) => selectFilteredChartData(state, 'revenue'));import { memo, useCallback } from 'react';
import { Table } from 'antd';
const ProductTable = memo(({ data, onEdit }: { data: any[]; onEdit: (id: string) => void }) => {
const handleEdit = useCallback((id: string) => {
onEdit(id);
}, [onEdit]);
const columns = useMemo(() => [
{ title: '名称', dataIndex: 'name' },
{ title: '价格', dataIndex: 'price', render: (val) => `¥${val}` },
{
title: '操作',
render: (_, record) => (
<Button onClick={() => handleEdit(record.id)}>编辑</Button>
),
},
], [handleEdit]);
return <Table columns={columns} dataSource={data} rowKey="id" />;
});对于大数据表格,使用 virtual 属性:
<Table
virtual
scroll={{ x: 1000, y: 600 }}
rowKey="id"
dataSource={largeData}
columns={columns}
pagination={false}
/>// components/ErrorBoundary.tsx
import React from 'react';
import { Result, Button } from 'antd';
export class ErrorBoundary extends React.Component<
{ children: React.ReactNode },
{ hasError: boolean; errorInfo: string }
> {
constructor(props: any) {
super(props);
this.state = { hasError: false, errorInfo: '' };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, errorInfo: error.message };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// 上报到 Sentry 或日志平台
console.error('Uncaught error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<Result
status="error"
title="页面出错了"
subTitle={this.state.errorInfo}
extra={
<Button type="primary" onClick={() => window.location.reload()}>
刷新重试
</Button>
}
/>
);
}
return this.props.children;
}
}本文通过一个完整的企业级实战案例,展示了 React 18 + Redux Toolkit + Ant Design 5 + Hooks 如何协同工作:
createAsyncThunk 让异步逻辑一目了然,配合 Immer 做到不可变更新零负担。useWatch)实现复杂联动,告别 shouldComponentUpdate 的繁琐。useRequest、useAuth)将业务逻辑抽离,使组件保持纯净,可测试性大幅提升。未来演进方向:可考虑引入 RTK Query 替代手写 Thunk 以进一步简化数据缓存与同步;结合 Zod 加强运行时类型校验;使用 Vite 替代 Webpack 加速构建。但核心架构思想不变——关注点分离、类型安全、开发者体验。
本文全部代码已脱敏并提取为可运行脚手架,欢迎实践并反馈。真正的工程化不是堆砌库,而是用合适的工具解决真实的业务痛点。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。