前端开发指南

中级 Intermediate 参考型 Reference claude-code
24 min read · 1190 lines

现代 React 开发综合指南:Suspense 数据获取、懒加载与性能优化

前端开发指南

目的

现代 React 开发综合指南,重点涵盖基于 Suspense 的数据获取、懒加载、规范的文件组织和性能优化。

适用场景

  • 创建新组件或页面
  • 构建新功能
  • 使用 TanStack Query 获取数据
  • 使用 TanStack Router 设置路由
  • 使用 MUI v7 为组件添加样式
  • 性能优化
  • 组织前端代码
  • TypeScript 最佳实践

导入别名速查

别名 解析到 示例
@/ src/ import { apiClient } from '@/lib/apiClient'
~types src/types import type { User } from '~types/user'
~components src/components import { SuspenseLoader } from '~components/SuspenseLoader'
~features src/features import { authApi } from '~features/auth'

常用导入速查表

// React 与懒加载
import React, { useState, useCallback, useMemo } from 'react';
const Heavy = React.lazy(() => import('./Heavy'));

// MUI 组件
import { Box, Paper, Typography, Button, Grid } from '@mui/material';
import type { SxProps, Theme } from '@mui/material';

// TanStack Query(Suspense)
import { useSuspenseQuery, useQueryClient } from '@tanstack/react-query';

// TanStack Router
import { createFileRoute } from '@tanstack/react-router';

// 项目组件
import { SuspenseLoader } from '~components/SuspenseLoader';

// Hooks
import { useAuth } from '@/hooks/useAuth';
import { useMuiSnackbar } from '@/hooks/useMuiSnackbar';

// 类型
import type { Post } from '~types/post';

组件模式

React.FC 模式(推荐)

所有组件使用 React.FC<Props> 模式,提供:

  • 明确的 props 类型安全
  • 一致的组件签名
  • 清晰的 prop 接口文档
  • 更好的 IDE 自动补全

基础模式

import React from 'react';

interface MyComponentProps {
    /** 要显示的用户 ID */
    userId: number;
    /** 操作发生时的可选回调 */
    onAction?: () => void;
}

export const MyComponent: React.FC<MyComponentProps> = ({ userId, onAction }) => {
    return (
        <div>
            用户:{userId}
        </div>
    );
};

export default MyComponent;

要点:

  • Props 接口单独定义,带 JSDoc 注释
  • React.FC<Props> 提供类型安全
  • 在参数中解构 props
  • 底部使用默认导出

懒加载模式

何时使用懒加载:

  • 重型组件(DataGrid、图表、富文本编辑器)
  • 路由级组件
  • 弹窗/对话框内容(初始不显示)
  • 首屏以下内容
import React from 'react';

// 懒加载重型组件
const PostDataGrid = React.lazy(() =>
    import('./grids/PostDataGrid')
);

// 命名导出的情况
const MyComponent = React.lazy(() =>
    import('./MyComponent').then(module => ({
        default: module.MyComponent
    }))
);

Suspense 边界

SuspenseLoader 组件

import { SuspenseLoader } from '~components/SuspenseLoader';

// 基本用法
<SuspenseLoader>
    <LazyLoadedComponent />
</SuspenseLoader>

功能:

  • 懒组件加载时显示加载指示器
  • 平滑的淡入动画
  • 一致的加载体验
  • 防止布局偏移

放置位置

路由级别:

const MyPage = lazy(() => import('@/features/my-feature/components/MyPage'));

function Route() {
    return (
        <SuspenseLoader>
            <MyPage />
        </SuspenseLoader>
    );
}

组件级别:

function ParentComponent() {
    return (
        <Box>
            <Header />
            <SuspenseLoader>
                <HeavyDataGrid />
            </SuspenseLoader>
        </Box>
    );
}

多个 Suspense 边界: 每个区块独立加载,用户体验更好。

function Page() {
    return (
        <Box>
            <SuspenseLoader>
                <HeaderSection />
            </SuspenseLoader>
            <SuspenseLoader>
                <MainContent />
            </SuspenseLoader>
            <SuspenseLoader>
                <Sidebar />
            </SuspenseLoader>
        </Box>
    );
}

组件结构模板

推荐顺序:

/**
 * 组件描述
 * 功能说明及使用场景
 */
import React, { useState, useCallback, useMemo, useEffect } from 'react';
import { Box, Paper, Button } from '@mui/material';
import type { SxProps, Theme } from '@mui/material';
import { useSuspenseQuery } from '@tanstack/react-query';

// 功能模块导入
import { myFeatureApi } from '../api/myFeatureApi';
import type { MyData } from '~types/myData';

// 组件导入
import { SuspenseLoader } from '~components/SuspenseLoader';

// Hooks
import { useAuth } from '@/hooks/useAuth';
import { useMuiSnackbar } from '@/hooks/useMuiSnackbar';

// 1. PROPS 接口(带 JSDoc)
interface MyComponentProps {
    /** 要显示的实体 ID */
    entityId: number;
    /** 操作完成时的可选回调 */
    onComplete?: () => void;
    /** 显示模式 */
    mode?: 'view' | 'edit';
}

// 2. 样式(内联且少于100行时)
const componentStyles: Record<string, SxProps<Theme>> = {
    container: {
        p: 2,
        display: 'flex',
        flexDirection: 'column',
    },
    header: {
        mb: 2,
        display: 'flex',
        justifyContent: 'space-between',
    },
};

// 3. 组件定义
export const MyComponent: React.FC<MyComponentProps> = ({
    entityId,
    onComplete,
    mode = 'view',
}) => {
    // 4. HOOKS(按以下顺序)
    // - 上下文 hooks 优先
    const { user } = useAuth();
    const { showSuccess, showError } = useMuiSnackbar();

    // - 数据获取
    const { data } = useSuspenseQuery({
        queryKey: ['myEntity', entityId],
        queryFn: () => myFeatureApi.getEntity(entityId),
    });

    // - 本地状态
    const [selectedItem, setSelectedItem] = useState<string | null>(null);

    // - 记忆化值
    const filteredData = useMemo(() => {
        return data.filter(item => item.active);
    }, [data]);

    // - 副作用
    useEffect(() => {
        // 初始化
        return () => {
            // 清理
        };
    }, []);

    // 5. 事件处理器(使用 useCallback)
    const handleItemSelect = useCallback((itemId: string) => {
        setSelectedItem(itemId);
    }, []);

    const handleSave = useCallback(async () => {
        try {
            await myFeatureApi.updateEntity(entityId, { /* 数据 */ });
            showSuccess('实体更新成功');
            onComplete?.();
        } catch (error) {
            showError('更新实体失败');
        }
    }, [entityId, onComplete, showSuccess, showError]);

    // 6. 渲染
    return (
        <Box sx={componentStyles.container}>
            <Box sx={componentStyles.header}>
                <h2>我的组件</h2>
                <Button onClick={handleSave}>保存</Button>
            </Box>
            <Paper sx={{ p: 2 }}>
                {filteredData.map(item => (
                    <div key={item.id}>{item.name}</div>
                ))}
            </Paper>
        </Box>
    );
};

// 7. 导出(底部默认导出)
export default MyComponent;

组件拆分原则

应拆分为多个组件的情况:

  • 组件超过 300 行
  • 存在多个不同职责
  • 有可复用的部分
  • 嵌套 JSX 复杂

应保持在同一文件的情况:

  • 组件少于 200 行
  • 逻辑紧密耦合
  • 不在其他地方复用
  • 简单的展示组件

导出模式(推荐:命名常量 + 默认导出)

export const MyComponent: React.FC<Props> = ({ ... }) => {
    // 组件逻辑
};

export default MyComponent;

原因:

  • 命名导出便于测试/重构
  • 默认导出便于懒加载
  • 消费者可选择使用哪种方式

数据获取

主要模式:useSuspenseQuery

所有新组件使用 useSuspenseQuery 而非普通 useQuery

优势:

  • 无需 isLoading 检查
  • 与 Suspense 边界集成
  • 组件代码更简洁
  • 一致的加载用户体验
  • 更好的错误处理(配合错误边界)

基本模式

import { useSuspenseQuery } from '@tanstack/react-query';
import { myFeatureApi } from '../api/myFeatureApi';

export const MyComponent: React.FC<Props> = ({ id }) => {
    // 无需 isLoading - Suspense 处理!
    const { data } = useSuspenseQuery({
        queryKey: ['myEntity', id],
        queryFn: () => myFeatureApi.getEntity(id),
    });

    // data 在这里始终有值(不是 undefined | Data)
    return <div>{data.name}</div>;
};

// 用 Suspense 边界包裹
<SuspenseLoader>
    <MyComponent id={123} />
</SuspenseLoader>

useSuspenseQuery 与 useQuery 对比

特性 useSuspenseQuery useQuery
加载状态 Suspense 处理 手动 isLoading 检查
数据类型 始终有值 Data | undefined
搭配使用 Suspense 边界 传统组件
推荐用于 新组件 仅遗留代码
错误处理 错误边界 手动错误状态

缓存优先策略

import { useSuspenseQuery, useQueryClient } from '@tanstack/react-query';

export function useSuspensePost(postId: number) {
    const queryClient = useQueryClient();

    return useSuspenseQuery({
        queryKey: ['post', postId],
        queryFn: async () => {
            // 策略1:先尝试从列表缓存获取
            const cachedListData = queryClient.getQueryData<{ posts: Post[] }>([
                'posts', 'list'
            ]);

            if (cachedListData?.posts) {
                const cachedPost = cachedListData.posts.find(
                    (post) => post.id === postId
                );
                if (cachedPost) {
                    return cachedPost;  // 从缓存返回!
                }
            }

            // 策略2:缓存中没有,从 API 获取
            return postApi.getPost(postId);
        },
        staleTime: 5 * 60 * 1000,      // 5分钟内视为新鲜
        gcTime: 10 * 60 * 1000,         // 缓存保留10分钟
        refetchOnWindowFocus: false,    // 窗口聚焦时不重新获取
    });
}

并行数据获取

import { useSuspenseQueries } from '@tanstack/react-query';

export const Dashboard: React.FC = () => {
    const [statsQuery, projectsQuery, notificationsQuery] = useSuspenseQueries({
        queries: [
            {
                queryKey: ['stats'],
                queryFn: () => statsApi.getStats(),
            },
            {
                queryKey: ['projects', 'active'],
                queryFn: () => projectsApi.getActiveProjects(),
            },
            {
                queryKey: ['notifications', 'unread'],
                queryFn: () => notificationsApi.getUnread(),
            },
        ],
    });

    return (
        <Box>
            <StatsCard data={statsQuery.data} />
            <ProjectsList projects={projectsQuery.data} />
            <Notifications items={notificationsQuery.data} />
        </Box>
    );
};

API 服务层模式

每个功能模块创建集中的 API 服务:

/**
 * 功能模块的集中 API 服务
 * 使用 apiClient 进行统一的错误处理
 */
import apiClient from '@/lib/apiClient';
import type { MyEntity, UpdatePayload } from '../types';

export const myFeatureApi = {
    /** 获取单个实体 */
    getEntity: async (blogId: number, entityId: number): Promise<MyEntity> => {
        const { data } = await apiClient.get(
            `/blog/entities/${blogId}/${entityId}`
        );
        return data;
    },

    /** 获取所有实体 */
    getEntities: async (blogId: number, view: 'summary' | 'flat'): Promise<MyEntity[]> => {
        const { data } = await apiClient.get(
            `/blog/entities/${blogId}`,
            { params: { view } }
        );
        return data.rows;
    },

    /** 更新实体 */
    updateEntity: async (
        blogId: number,
        entityId: number,
        payload: UpdatePayload
    ): Promise<MyEntity> => {
        const { data } = await apiClient.put(
            `/blog/entities/${blogId}/${entityId}`,
            payload
        );
        return data;
    },

    /** 删除实体 */
    deleteEntity: async (blogId: number, entityId: number): Promise<void> => {
        await apiClient.delete(`/blog/entities/${blogId}/${entityId}`);
    },
};

要点:

  • 导出单个对象,包含所有方法
  • 使用 apiClient(来自 @/lib/apiClient 的 axios 实例)
  • 类型安全的参数和返回值
  • 每个方法带 JSDoc 注释
  • 集中式错误处理(apiClient 负责)

路由格式规则(重要)

// 正确 - 直接使用服务路径
await apiClient.get('/blog/posts/123');
await apiClient.post('/projects/create', data);

// 错误 - 不要添加 /api/ 前缀
await apiClient.get('/api/blog/posts/123');  // 错误!

原因: API 路由由代理配置处理,无需 /api/ 前缀。

变更操作(Mutations)

import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useMuiSnackbar } from '@/hooks/useMuiSnackbar';

export const MyComponent: React.FC = () => {
    const queryClient = useQueryClient();
    const { showSuccess, showError } = useMuiSnackbar();

    const updateMutation = useMutation({
        mutationFn: (payload: UpdatePayload) =>
            myFeatureApi.updateEntity(blogId, entityId, payload),

        onSuccess: () => {
            // 使相关查询失效并重新获取
            queryClient.invalidateQueries({
                queryKey: ['entity', blogId, entityId]
            });
            showSuccess('实体更新成功');
        },

        onError: (error) => {
            showError('更新实体失败');
            console.error('更新错误:', error);
        },
    });

    const handleUpdate = () => {
        updateMutation.mutate({ name: '新名称' });
    };

    return (
        <Button
            onClick={handleUpdate}
            disabled={updateMutation.isPending}
        >
            {updateMutation.isPending ? '更新中...' : '更新'}
        </Button>
    );
};

乐观更新

const updateMutation = useMutation({
    mutationFn: (payload) => myFeatureApi.update(id, payload),

    // 乐观更新
    onMutate: async (newData) => {
        // 取消正在进行的重新获取
        await queryClient.cancelQueries({ queryKey: ['entity', id] });

        // 快照当前值
        const previousData = queryClient.getQueryData(['entity', id]);

        // 乐观更新缓存
        queryClient.setQueryData(['entity', id], (old) => ({
            ...old,
            ...newData,
        }));

        // 返回回滚函数
        return { previousData };
    },

    // 出错时回滚
    onError: (err, newData, context) => {
        queryClient.setQueryData(['entity', id], context.previousData);
        showError('更新失败');
    },

    // 成功或失败后重新获取
    onSettled: () => {
        queryClient.invalidateQueries({ queryKey: ['entity', id] });
    },
});

文件组织

features/ 与 components/ 的区别

features/ 目录

用途:领域特定功能,拥有自己的逻辑、API 和组件

使用场景:

  • 功能模块包含多个相关组件
  • 功能模块有自己的 API 端点
  • 存在领域特定逻辑
  • 需要自定义 hooks/工具函数

结构:

features/
  my-feature/
    api/
      myFeatureApi.ts         # API 服务层
    components/
      MyFeatureMain.tsx       # 主组件
      SubComponents/          # 相关组件
    hooks/
      useMyFeature.ts         # 自定义 hooks
      useSuspenseMyFeature.ts # Suspense hooks
    helpers/
      myFeatureHelpers.ts     # 工具函数
    types/
      index.ts                # TypeScript 类型
    index.ts                  # 公共导出

components/ 目录

用途:真正可复用的组件,跨多个功能模块使用

使用场景:

  • 组件在 3 个以上位置使用
  • 组件是通用的(无功能特定逻辑)
  • 组件是 UI 基础元素或模式

示例: SuspenseLoader/CustomAppBar/ErrorBoundary/

文件命名规范

类型 命名模式 示例
组件 PascalCase + .tsx MyComponent.tsx
Hooks camelCase + use 前缀 + .ts useMyFeature.ts
API 服务 camelCase + Api 后缀 + .ts myFeatureApi.ts
工具函数 camelCase + .ts myFeatureHelpers.ts
类型 camelCase + .ts types/index.ts

导入顺序(推荐)

// 1. React 及 React 相关
import React, { useState, useCallback, useMemo } from 'react';

// 2. 第三方库(按字母排序)
import { Box, Paper, Button } from '@mui/material';
import { useSuspenseQuery } from '@tanstack/react-query';

// 3. 别名导入(@ 优先,然后 ~)
import { apiClient } from '@/lib/apiClient';
import { SuspenseLoader } from '~components/SuspenseLoader';

// 4. 类型导入(分组)
import type { Post } from '~types/post';
import type { User } from '~types/user';

// 5. 相对路径导入(同一功能模块内)
import { MySubComponent } from './MySubComponent';

完整目录结构一览

src/
├── features/                    # 领域特定功能
│   ├── posts/
│   │   ├── api/
│   │   ├── components/
│   │   ├── hooks/
│   │   ├── helpers/
│   │   ├── types/
│   │   └── index.ts
│   ├── blogs/
│   └── auth/
│
├── components/                  # 可复用组件
│   ├── SuspenseLoader/
│   ├── CustomAppBar/
│   └── ErrorBoundary/
│
├── routes/                      # TanStack Router 路由
│   ├── __root.tsx
│   ├── index.tsx
│   └── my-route/
│       └── index.tsx
│
├── hooks/                       # 共享 hooks
│   ├── useAuth.ts
│   └── useMuiSnackbar.ts
│
├── lib/                         # 共享工具
│   └── apiClient.ts
│
├── types/                       # 共享 TypeScript 类型
│   ├── user.ts
│   └── post.ts
│
└── App.tsx                      # 根组件

样式指南

内联 vs 独立文件

条件 方案
少于 100 行 组件顶部内联定义
超过 100 行 独立 .styles.ts 文件

内联样式示例

import type { SxProps, Theme } from '@mui/material';

const componentStyles: Record<string, SxProps<Theme>> = {
    container: {
        p: 2,
        display: 'flex',
        flexDirection: 'column',
    },
    header: {
        mb: 2,
        borderBottom: '1px solid',
        borderColor: 'divider',
    },
};

独立文件示例

// MyComponent.styles.ts
import type { SxProps, Theme } from '@mui/material';

export const componentStyles: Record<string, SxProps<Theme>> = {
    container: { ... },
    header: { ... },
    // 超过100行的样式...
};

// MyComponent.tsx
import { componentStyles } from './MyComponent.styles';

sx 属性模式

// 基本用法
<Box sx={{ p: 2, mb: 3, display: 'flex' }}>内容</Box>

// 访问主题
<Box sx={{
    p: 2,
    backgroundColor: (theme) => theme.palette.primary.main,
    color: (theme) => theme.palette.primary.contrastText,
}} />

// 响应式样式
<Box sx={{
    p: { xs: 1, sm: 2, md: 3 },
    width: { xs: '100%', md: '50%' },
}} />

// 伪选择器
<Box sx={{
    '&:hover': { backgroundColor: 'rgba(0,0,0,0.05)' },
    '& .child-class': { color: 'primary.main' },
}} />

MUI v7 Grid(重要语法变更)

// 正确 - v7 语法,使用 size 属性
<Grid container spacing={2}>
    <Grid size={{ xs: 12, md: 6 }}>左栏</Grid>
    <Grid size={{ xs: 12, md: 6 }}>右栏</Grid>
</Grid>

// 错误 - 旧的 v6 语法
<Grid container spacing={2}>
    <Grid xs={12} md={6}>内容</Grid>  {/* 旧语法 - 不要使用 */}
</Grid>

禁止使用的样式方案

  • makeStyles(MUI v4 模式)- 已弃用
  • styled() 组件 - sx 属性更灵活

代码风格标准

  • 缩进:4 个空格
  • 引号:单引号
  • 尾逗号:始终使用

路由指南

TanStack Router 基于文件夹的路由

routes/
  __root.tsx                    # 根布局
  index.tsx                     # 首页路由 (/)
  posts/
    index.tsx                   # /posts
    create/
      index.tsx                 # /posts/create
    $postId.tsx                 # /posts/:postId(动态参数)

基本路由模式

import { createFileRoute } from '@tanstack/react-router';
import { lazy } from 'react';

// 懒加载页面组件
const PostsList = lazy(() =>
    import('@/features/posts/components/PostsList').then(
        (module) => ({ default: module.PostsList }),
    ),
);

export const Route = createFileRoute('/posts/')({
    component: PostsPage,
    // 定义面包屑数据
    loader: () => ({
        crumb: '文章列表',
    }),
});

function PostsPage() {
    return <PostsList title='所有文章' showFilters={true} />;
}

动态路由

// routes/users/$userId.tsx
export const Route = createFileRoute('/users/$userId')({
    component: UserPage,
});

function UserPage() {
    const { userId } = Route.useParams();
    return <UserProfile userId={userId} />;
}

编程式导航

import { useNavigate } from '@tanstack/react-router';

const navigate = useNavigate();

// 基本导航
navigate({ to: '/posts' });

// 带参数
navigate({ to: '/users/$userId', params: { userId: '123' } });

// 带查询参数
navigate({ to: '/search', search: { query: 'test', page: 1 } });

加载与错误状态

严格规则:绝不使用提前返回(Early Return)

// 错误 - 导致布局偏移
if (isLoading) {
    return <LoadingSpinner />;
}

// 正确 - 保持一致的布局
<SuspenseLoader>
    <Content />
</SuspenseLoader>

原因:

  1. 布局偏移:加载完成时内容位置跳动
  2. CLS(累积布局偏移):Core Web Vitals 评分差
  3. 糟糕的用户体验:页面结构突然变化
  4. 滚动位置丢失:用户在页面中的位置丢失

加载方案优先级

  1. 首选:SuspenseLoader + useSuspenseQuery(现代模式)
  2. 可接受:LoadingOverlay(遗留模式)
  3. 尚可:Skeleton 骨架屏(保持相同布局)
  4. 禁止:提前返回或条件布局

错误处理

必须使用 useMuiSnackbar,禁止使用 react-toastify

import { useMuiSnackbar } from '@/hooks/useMuiSnackbar';

const { showSuccess, showError, showInfo, showWarning } = useMuiSnackbar();

// 可用方法:
// showSuccess(message)  - 绿色成功消息
// showError(message)    - 红色错误消息
// showWarning(message)  - 橙色警告消息
// showInfo(message)     - 蓝色信息消息

错误边界

import { ErrorBoundary } from 'react-error-boundary';

<ErrorBoundary
    FallbackComponent={ErrorFallback}
    onError={(error) => console.error('边界捕获:', error)}
>
    <SuspenseLoader>
        <ComponentThatMightError />
    </SuspenseLoader>
</ErrorBoundary>

性能优化

useMemo - 昂贵计算

// 正确 - 记忆化,仅在依赖变化时重新计算
const filteredItems = useMemo(() => {
    return items
        .filter(item => item.name.toLowerCase().includes(searchTerm.toLowerCase()))
        .sort((a, b) => a.name.localeCompare(b.name));
}, [items, searchTerm]);

适用场景: 过滤/排序大数组、复杂计算、数据结构转换

useCallback - 传递给子组件的事件处理器

// 正确 - 稳定的函数引用
const handleClick = useCallback((id: string) => {
    console.log('点击:', id);
}, []);

// 子组件仅在 props 实际变化时重新渲染
return <Child onClick={handleClick} />;

React.memo - 组件记忆化

// 包裹昂贵组件
export const ExpensiveComponent = React.memo<ExpensiveComponentProps>(
    function ExpensiveComponent({ data, onAction }) {
        return <ComplexVisualization data={data} />;
    }
);

防抖搜索

const [searchTerm, setSearchTerm] = useState('');
const [debouncedSearchTerm] = useDebounce(searchTerm, 300);

// 查询使用防抖后的值
const { data } = useSuspenseQuery({
    queryKey: ['search', debouncedSearchTerm],
    queryFn: () => api.search(debouncedSearchTerm),
});

最佳防抖时间:

  • 300-500ms:搜索/过滤
  • 1000ms:自动保存
  • 100-200ms:实时校验

内存泄漏防护

useEffect(() => {
    const intervalId = setInterval(() => {
        setCount(c => c + 1);
    }, 1000);

    return () => {
        clearInterval(intervalId);  // 清理!
    };
}, []);

懒加载重型依赖

// 错误 - 顶层导入重型库
import jsPDF from 'jspdf';

// 正确 - 需要时动态导入
const handleExportPDF = async () => {
    const { jsPDF } = await import('jspdf');
    const doc = new jsPDF();
    // 使用
};

TypeScript 标准

严格模式

// tsconfig.json
{
    "compilerOptions": {
        "strict": true,
        "noImplicitAny": true,
        "strictNullChecks": true
    }
}

禁止使用 any 类型

// 错误
function handleData(data: any) { return data.something; }

// 正确 - 使用具体类型
interface MyData { something: string; }
function handleData(data: MyData) { return data.something; }

// 正确 - 真正未知的数据使用 unknown
function handleUnknown(data: unknown) {
    if (typeof data === 'object' && data !== null && 'something' in data) {
        return (data as MyData).something;
    }
}

类型导入

// 正确 - 明确标记为类型导入
import type { User } from '~types/user';
import type { SxProps, Theme } from '@mui/material';

// 避免 - 混合值和类型导入
import { User } from '~types/user';  // 不清楚是类型还是值

实用类型

Partial<T>     // 所有属性变为可选
Pick<T, K>     // 选择特定属性
Omit<T, K>     // 排除特定属性
Required<T>    // 所有属性变为必需
Record<K, V>   // 类型安全的对象/映射

常见模式

认证:useAuth Hook

import { useAuth } from '@/hooks/useAuth';

const { user } = useAuth();
// user.id, user.email, user.username, user.roles

绝不直接调用认证 API,始终使用 useAuth Hook。

表单:React Hook Form + Zod

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const formSchema = z.object({
    username: z.string().min(3, '用户名至少3个字符'),
    email: z.string().email('无效的邮箱地址'),
});

type FormData = z.infer<typeof formSchema>;

const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
    resolver: zodResolver(formSchema),
});

对话框标准结构

所有对话框应包含:标题中的图标、关闭按钮(X)、底部操作按钮。

状态管理

数据类型 方案 说明
服务器数据 TanStack Query 主要方案
局部 UI 状态 useState 弹窗开关、选中标签等
全局客户端状态 Zustand 仅用于主题偏好等少量场景

核心原则

  1. 懒加载所有重型内容:路由、DataGrid、图表、编辑器
  2. 用 Suspense 处理加载:使用 SuspenseLoader,而非提前返回
  3. useSuspenseQuery:新代码的主要数据获取模式
  4. 功能模块规范组织:api/、components/、hooks/、helpers/ 子目录
  5. 样式按大小决定:少于100行内联,超过100行独立文件
  6. 导入别名:使用 @/、~types、~components、~features
  7. 禁止提前返回:防止布局偏移
  8. useMuiSnackbar:所有用户通知统一使用

相关技能 Related Skills