React全家桶实战:Redux+Antd+Hooks构建企业级前端应用

React全家桶实战:Redux+Antd+Hooks构建企业级前端应用

本文深入探讨React生态核心技术的整合应用,通过完整的企业级项目实战,展示Redux状态管理、Antd组件库、React Hooks的协同开发模式,提供可直接落地的架构方案与性能优化策略。

一、技术选型与架构设计

现代React应用开发已形成稳定的技术栈组合。本文选取以下核心依赖:

json

perl 复制代码
{
  "react": "^18.2.0",
  "react-dom": "^18.2.0",
  "react-redux": "^8.0.5",
  "@reduxjs/toolkit": "^1.9.3",
  "antd": "^5.1.0",
  "axios": "^1.3.0",
  "react-router-dom": "^6.8.0"
}

架构分层设计采用Ducks模式 整合Redux代码,配合Hooks封装实现逻辑复用,Antd作为UI基础组件库。

text

bash 复制代码
src/
├── api/              # 接口层
├── store/            # Redux状态管理
│   ├── slices/       # RTK切片
│   └── index.ts      # Store配置
├── hooks/            # 自定义Hooks
├── components/       # 公共组件
├── pages/            # 页面组件
└── utils/            # 工具函数

二、Redux Toolkit状态管理实践

2.1 Store配置与切片设计

使用Redux Toolkit简化Redux配置,通过createSlice自动生成action creators:

typescript

typescript 复制代码
// store/slices/userSlice.ts
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { userApi } from '@/api/user';

export interface UserState {
  profile: UserProfile | null;
  loading: boolean;
  error: string | null;
}

const initialState: UserState = {
  profile: null,
  loading: false,
  error: null
};

export const fetchUserProfile = createAsyncThunk(
  'user/fetchProfile',
  async (userId: string, { rejectWithValue }) => {
    try {
      const response = await userApi.getProfile(userId);
      return response.data;
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);

const userSlice = createSlice({
  name: 'user',
  initialState,
  reducers: {
    clearUser: (state) => {
      state.profile = null;
      state.error = null;
    },
    updateUser: (state, action) => {
      state.profile = { ...state.profile, ...action.payload };
    }
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchUserProfile.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchUserProfile.fulfilled, (state, action) => {
        state.loading = false;
        state.profile = action.payload;
      })
      .addCase(fetchUserProfile.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload as string;
      });
  }
});

export const { clearUser, updateUser } = userSlice.actions;
export default userSlice.reducer;

2.2 Store组合与类型定义

typescript

typescript 复制代码
// store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import userReducer from './slices/userSlice';
import appReducer from './slices/appSlice';

export const store = configureStore({
  reducer: {
    user: userReducer,
    app: appReducer
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: false,
      thunk: true
    }),
  devTools: process.env.NODE_ENV !== 'production'
});

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

2.3 类型化Hooks封装

typescript

typescript 复制代码
// hooks/redux.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from '@/store';

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

三、Antd组件系统集成

3.1 主题定制与全局配置

Antd 5.x支持CSS-in-JS主题动态切换:

typescript

javascript 复制代码
// App.tsx
import { ConfigProvider, theme, App as AntApp } from 'antd';
import { useSelector } from 'react-redux';
import { RootState } from '@/store';

const App: React.FC = () => {
  const themeMode = useSelector((state: RootState) => state.app.theme);
  
  return (
    <ConfigProvider
      theme={{
        algorithm: themeMode === 'dark' 
          ? theme.darkAlgorithm 
          : theme.defaultAlgorithm,
        token: {
          colorPrimary: '#1890ff',
          borderRadius: 8,
          fontSize: 14
        },
        components: {
          Table: {
            headerBg: '#fafafa',
            rowHoverBg: '#e6f7ff'
          }
        }
      }}
    >
      <AntApp>
        <RouterProvider router={router} />
      </AntApp>
    </ConfigProvider>
  );
};

3.2 高阶组件封装

封装可复用的表格组件,集成分页、筛选功能:

tsx

typescript 复制代码
// components/DataTable/index.tsx
import { Table, Space, Button, Input, Form, Modal } from 'antd';
import { SearchOutlined, PlusOutlined } from '@ant-design/icons';
import { useAntdTable } from '@/hooks/useAntdTable';

interface DataTableProps<T> {
  columns: ColumnsType<T>;
  fetchData: (params: any) => Promise<{ list: T[]; total: number }>;
  searchFields?: SearchField[];
  onCreate?: () => void;
}

export function DataTable<T extends object>({
  columns,
  fetchData,
  searchFields,
  onCreate
}: DataTableProps<T>) {
  const [form] = Form.useForm();
  const { data, loading, pagination, search, refresh } = useAntdTable({
    fetchData,
    form,
    defaultPageSize: 20
  });

  return (
    <div className="data-table-container">
      <div className="table-header">
        <Form form={form} layout="inline" onFinish={search}>
          {searchFields?.map(field => (
            <Form.Item key={field.name} name={field.name}>
              {field.type === 'input' && (
                <Input placeholder={field.placeholder} allowClear />
              )}
              {field.type === 'select' && (
                <Select options={field.options} placeholder={field.placeholder} />
              )}
            </Form.Item>
          ))}
          <Form.Item>
            <Button type="primary" htmlType="submit" icon={<SearchOutlined />}>
              查询
            </Button>
            <Button onClick={() => form.resetFields()} style={{ marginLeft: 8 }}>
              重置
            </Button>
          </Form.Item>
        </Form>
        {onCreate && (
          <Button type="primary" icon={<PlusOutlined />} onClick={onCreate}>
            新增
          </Button>
        )}
      </div>
      
      <Table
        columns={columns}
        dataSource={data}
        loading={loading}
        pagination={{
          ...pagination,
          showSizeChanger: true,
          showQuickJumper: true,
          showTotal: (total) => `共 ${total} 条`
        }}
        rowKey="id"
        scroll={{ x: 'max-content' }}
      />
    </div>
  );
}

四、React Hooks深度应用

4.1 自定义数据请求Hook

typescript

typescript 复制代码
// hooks/useRequest.ts
import { useState, useEffect, useCallback, useRef } from 'react';
import { message } from 'antd';

interface UseRequestOptions<T> {
  manual?: boolean;
  onSuccess?: (data: T) => void;
  onError?: (error: Error) => void;
  debounce?: number;
  retryCount?: number;
}

export function useRequest<T, P extends any[] = any[]>(
  service: (...args: P) => Promise<T>,
  options: UseRequestOptions<T> = {}
) {
  const { manual = false, onSuccess, onError, debounce, retryCount = 0 } = options;
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);
  const timerRef = useRef<NodeJS.Timeout | null>(null);
  const retryRef = useRef(0);

  const run = useCallback(
    async (...args: P) => {
      if (debounce) {
        return new Promise<T>((resolve, reject) => {
          if (timerRef.current) clearTimeout(timerRef.current);
          timerRef.current = setTimeout(async () => {
            try {
              const result = await executeRequest(...args);
              resolve(result);
            } catch (err) {
              reject(err);
            }
          }, debounce);
        });
      }
      return executeRequest(...args);
    },
    [service, debounce]
  );

  const executeRequest = async (...args: P): Promise<T> => {
    setLoading(true);
    setError(null);
    try {
      const result = await service(...args);
      setData(result);
      setLoading(false);
      onSuccess?.(result);
      retryRef.current = 0;
      return result;
    } catch (err) {
      const error = err instanceof Error ? err : new Error(String(err));
      setError(error);
      setLoading(false);
      onError?.(error);
      
      if (retryRef.current < retryCount) {
        retryRef.current++;
        return executeRequest(...args);
      }
      
      message.error(error.message || '请求失败');
      throw error;
    }
  };

  useEffect(() => {
    if (!manual) {
      run(...([] as unknown as P));
    }
    return () => {
      if (timerRef.current) clearTimeout(timerRef.current);
    };
  }, [manual]);

  return { data, loading, error, run, refresh: () => run(...([] as unknown as P)) };
}

4.2 防抖搜索Hook

tsx

typescript 复制代码
// hooks/useDebounceSearch.ts
import { useState, useEffect, useCallback } from 'react';
import { useAppDispatch, useAppSelector } from './redux';

export function useDebounceSearch<T>(
  searchAction: (params: any) => any,
  debounceDelay: number = 300
) {
  const [keyword, setKeyword] = useState('');
  const [debouncedKeyword, setDebouncedKeyword] = useState('');
  const dispatch = useAppDispatch();
  const data = useAppSelector((state) => state.search.data);
  const loading = useAppSelector((state) => state.search.loading);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedKeyword(keyword);
    }, debounceDelay);

    return () => clearTimeout(timer);
  }, [keyword, debounceDelay]);

  useEffect(() => {
    if (debouncedKeyword !== undefined) {
      dispatch(searchAction({ keyword: debouncedKeyword }));
    }
  }, [debouncedKeyword, dispatch, searchAction]);

  const handleSearch = useCallback((value: string) => {
    setKeyword(value);
  }, []);

  return { data, loading, keyword, handleSearch };
}

4.3 响应式断点Hook

typescript

ini 复制代码
// hooks/useResponsive.ts
import { useState, useEffect } from 'react';

type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';

const breakpoints: Record<Breakpoint, number> = {
  xs: 480,
  sm: 576,
  md: 768,
  lg: 992,
  xl: 1200,
  xxl: 1600
};

export function useResponsive() {
  const [currentBreakpoint, setCurrentBreakpoint] = useState<Breakpoint>('lg');

  useEffect(() => {
    const handleResize = () => {
      const width = window.innerWidth;
      let bp: Breakpoint = 'xs';
      for (const [key, value] of Object.entries(breakpoints)) {
        if (width >= value) {
          bp = key as Breakpoint;
        }
      }
      setCurrentBreakpoint(bp);
    };

    handleResize();
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  const isMobile = currentBreakpoint === 'xs' || currentBreakpoint === 'sm';
  const isTablet = currentBreakpoint === 'md' || currentBreakpoint === 'lg';
  const isDesktop = currentBreakpoint === 'xl' || currentBreakpoint === 'xxl';

  return { currentBreakpoint, isMobile, isTablet, isDesktop };
}

五、数据请求与状态管理整合

5.1 Axios封装与拦截器

typescript

typescript 复制代码
// utils/request.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { message } from 'antd';
import { store } from '@/store';
import { logout } from '@/store/slices/userSlice';

class Request {
  private instance: AxiosInstance;

  constructor(baseURL: string) {
    this.instance = axios.create({
      baseURL,
      timeout: 30000,
      headers: {
        'Content-Type': 'application/json'
      }
    });

    this.setupInterceptors();
  }

  private setupInterceptors() {
    this.instance.interceptors.request.use(
      (config) => {
        const token = localStorage.getItem('accessToken');
        if (token) {
          config.headers.Authorization = `Bearer ${token}`;
        }
        return config;
      },
      (error) => Promise.reject(error)
    );

    this.instance.interceptors.response.use(
      (response: AxiosResponse) => {
        const { code, data, message: msg } = response.data;
        if (code === 200) {
          return data;
        }
        message.error(msg || '请求失败');
        return Promise.reject(new Error(msg));
      },
      (error) => {
        if (error.response) {
          const { status } = error.response;
          if (status === 401) {
            store.dispatch(logout());
            message.error('登录已过期,请重新登录');
            window.location.href = '/login';
          } else if (status === 403) {
            message.error('无权限访问');
          } else if (status === 500) {
            message.error('服务器内部错误');
          } else {
            message.error(error.response.data?.message || '请求失败');
          }
        } else if (error.code === 'ECONNABORTED') {
          message.error('请求超时');
        } else {
          message.error('网络异常');
        }
        return Promise.reject(error);
      }
    );
  }

  public get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.get(url, config);
  }

  public post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.post(url, data, config);
  }

  public put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.put(url, data, config);
  }

  public delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.delete(url, config);
  }
}

export const request = new Request(process.env.REACT_APP_API_URL || '/api');

5.2 业务页面完整示例

tsx

typescript 复制代码
// pages/UserManagement/index.tsx
import React, { useEffect } from 'react';
import { Card, Space, Tag, Button, Modal, Form, Input, Select } from 'antd';
import { EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useRequest } from '@/hooks/useRequest';
import { DataTable } from '@/components/DataTable';
import { fetchUsers, deleteUser, updateUser } from '@/store/slices/userSlice';

const UserManagement: React.FC = () => {
  const dispatch = useAppDispatch();
  const { users, loading, total } = useAppSelector((state) => state.user);
  const [form] = Form.useForm();
  const [modalVisible, setModalVisible] = useState(false);
  const [editingUser, setEditingUser] = useState<User | null>(null);

  const { run: handleDelete } = useRequest(
    async (id: string) => {
      await dispatch(deleteUser(id)).unwrap();
      message.success('删除成功');
      refreshTable();
    },
    { manual: true }
  );

  const columns: ColumnsType<User> = [
    {
      title: '用户名',
      dataIndex: 'username',
      key: 'username',
      render: (text) => <span className="font-medium">{text}</span>
    },
    {
      title: '角色',
      dataIndex: 'role',
      key: 'role',
      render: (role) => (
        <Tag color={role === 'admin' ? 'red' : 'blue'}>
          {role === 'admin' ? '管理员' : '普通用户'}
        </Tag>
      )
    },
    {
      title: '状态',
      dataIndex: 'status',
      key: 'status',
      render: (status) => (
        <Tag color={status === 'active' ? 'green' : 'gray'}>
          {status === 'active' ? '活跃' : '禁用'}
        </Tag>
      )
    },
    {
      title: '操作',
      key: 'action',
      render: (_, record) => (
        <Space>
          <Button type="link" icon={<EyeOutlined />} onClick={() => handleView(record)}>
            查看
          </Button>
          <Button type="link" icon={<EditOutlined />} onClick={() => handleEdit(record)}>
            编辑
          </Button>
          <Button 
            type="link" 
            danger 
            icon={<DeleteOutlined />} 
            onClick={() => handleDelete(record.id)}
          >
            删除
          </Button>
        </Space>
      )
    }
  ];

  const fetchData = async (params: any) => {
    const result = await dispatch(fetchUsers(params)).unwrap();
    return { list: result.list, total: result.total };
  };

  const handleEdit = (user: User) => {
    setEditingUser(user);
    form.setFieldsValue(user);
    setModalVisible(true);
  };

  const handleSubmit = async () => {
    const values = await form.validateFields();
    await dispatch(updateUser({ id: editingUser!.id, ...values })).unwrap();
    message.success('更新成功');
    setModalVisible(false);
    refreshTable();
  };

  return (
    <Card title="用户管理" className="user-management">
      <DataTable<User>
        columns={columns}
        fetchData={fetchData}
        searchFields={[
          { name: 'username', type: 'input', placeholder: '请输入用户名' },
          { 
            name: 'role', 
            type: 'select', 
            placeholder: '请选择角色',
            options: [
              { label: '管理员', value: 'admin' },
              { label: '普通用户', value: 'user' }
            ]
          }
        ]}
      />
      
      <Modal
        title="编辑用户"
        open={modalVisible}
        onOk={handleSubmit}
        onCancel={() => setModalVisible(false)}
        width={600}
      >
        <Form form={form} layout="vertical">
          <Form.Item
            name="username"
            label="用户名"
            rules={[{ required: true, message: '请输入用户名' }]}
          >
            <Input />
          </Form.Item>
          <Form.Item
            name="email"
            label="邮箱"
            rules={[{ type: 'email', message: '请输入有效邮箱' }]}
          >
            <Input />
          </Form.Item>
          <Form.Item name="role" label="角色">
            <Select>
              <Select.Option value="admin">管理员</Select.Option>
              <Select.Option value="user">普通用户</Select.Option>
            </Select>
          </Form.Item>
        </Form>
      </Modal>
    </Card>
  );
};

export default UserManagement;

六、性能优化策略

6.1 组件懒加载

tsx

javascript 复制代码
// router/index.tsx
import { lazy, Suspense } from 'react';
import { Spin } from 'antd';

const Dashboard = lazy(() => import('@/pages/Dashboard'));
const UserManagement = lazy(() => import('@/pages/UserManagement'));
const Settings = lazy(() => import('@/pages/Settings'));

const withSuspense = (Component: React.LazyExoticComponent<React.ComponentType>) => (
  <Suspense fallback={<Spin size="large" className="global-spin" />}>
    <Component />
  </Suspense>
);

export const routes = [
  {
    path: '/dashboard',
    element: withSuspense(Dashboard)
  },
  {
    path: '/users',
    element: withSuspense(UserManagement)
  },
  {
    path: '/settings',
    element: withSuspense(Settings)
  }
];

6.2 memo与useCallback优化

tsx

typescript 复制代码
// components/UserCard/index.tsx
import React, { memo, useCallback } from 'react';
import { Card, Avatar, Button } from 'antd';

interface UserCardProps {
  user: User;
  onEdit: (id: string) => void;
  onDelete: (id: string) => void;
}

const UserCard = memo(({ user, onEdit, onDelete }: UserCardProps) => {
  const handleEdit = useCallback(() => {
    onEdit(user.id);
  }, [onEdit, user.id]);

  const handleDelete = useCallback(() => {
    onDelete(user.id);
  }, [onDelete, user.id]);

  return (
    <Card>
      <Card.Meta
        avatar={<Avatar src={user.avatar} />}
        title={user.username}
        description={user.email}
      />
      <Space>
        <Button onClick={handleEdit}>编辑</Button>
        <Button danger onClick={handleDelete}>删除</Button>
      </Space>
    </Card>
  );
});

UserCard.displayName = 'UserCard';
export default UserCard;

6.3 选择性Redux订阅

typescript

javascript 复制代码
// hooks/useUserProfile.ts
import { useAppSelector } from './redux';
import { createSelector } from '@reduxjs/toolkit';

const selectUserProfile = createSelector(
  (state: RootState) => state.user.profile,
  (profile) => ({
    id: profile?.id,
    name: profile?.username,
    avatar: profile?.avatar
  })
);

export function useUserProfile() {
  return useAppSelector(selectUserProfile);
}

七、错误边界与异常处理

tsx

typescript 复制代码
// components/ErrorBoundary/index.tsx
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { Result, Button } from 'antd';

interface Props {
  children: ReactNode;
  fallback?: ReactNode;
}

interface State {
  hasError: boolean;
  error: Error | null;
}

export class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error('ErrorBoundary caught:', error, errorInfo);
    // 上报错误到监控系统
    reportError(error, errorInfo);
  }

  handleReset = () => {
    this.setState({ hasError: false, error: null });
  };

  render() {
    if (this.state.hasError) {
      return this.props.fallback || (
        <Result
          status="error"
          title="页面加载出错"
          subTitle={this.state.error?.message || '发生了未知错误'}
          extra={
            <Button type="primary" onClick={this.handleReset}>
              重试
            </Button>
          }
        />
      );
    }

    return this.props.children;
  }
}

八、总结

本文完整展示了React全家桶在企业级应用中的实战方案:

  1. 状态管理:Redux Toolkit提供类型安全的store配置,Ducks模式组织代码结构
  2. UI组件:Antd 5.x主题系统与组件深度集成,封装高阶组件提升复用性
  3. 逻辑复用:自定义Hooks封装数据请求、防抖、响应式等通用逻辑
  4. 请求层:Axios拦截器统一错误处理,与Redux联动实现全局状态更新
  5. 性能优化:组件懒加载、memo缓存、选择性订阅减少不必要渲染

完整的项目源码已同步至GitHub仓库,包含详细的部署文档和测试用例。在实际项目中,建议结合TypeScript严格模式、ESLint规范、Husky Git钩子保证代码质量,并接入Sentry等监控工具完善生产环境可观测性。

相关推荐
求索实验室2 小时前
1368次实验证明:PostgreSQL对数据倾斜的基数估计无解——我的AI4S参赛实录
postgresql·ai编程
AI编程实验室2 小时前
Node.js Agent Handoff 仓库扫描 MVP:忽略规则、include 通配与稳定输出实现
前端·ai编程
奈斯先生vector3 小时前
DeepSeek Harness 插件怎么做才不把权限带进 Agent:从一个只读代码审查器开始
aigc·ai编程
XDevelop AI智能应用软件开发5 小时前
AI演进下的“第五次软件危机”与软件工程重塑
人工智能·软件工程·ai编程·软件危机
黑科技iOS上架5 小时前
Qwen3.8-27B日常量化版在 Mac M5 32G配置上表现如何
经验分享·ai编程
HelloDong6 小时前
AI 说「修好了」,凭什么信
人工智能·ai编程·claude
QCodingDev6 小时前
Spring AI Alibaba ReAct Agent实战:从Tool Calling到Agent,企业AI复杂业务该如何设计?
java·人工智能·agent·ai编程·spring ai
Jay-r6 小时前
DeepSeek Harness 极简上手:装好、玩熟、让它自己长新能力
人工智能·windows·ai·github·ai编程·deepseek·harness