前端开发指南
目的
现代 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>
原因:
- 布局偏移:加载完成时内容位置跳动
- CLS(累积布局偏移):Core Web Vitals 评分差
- 糟糕的用户体验:页面结构突然变化
- 滚动位置丢失:用户在页面中的位置丢失
加载方案优先级
- 首选:SuspenseLoader + useSuspenseQuery(现代模式)
- 可接受:LoadingOverlay(遗留模式)
- 尚可:Skeleton 骨架屏(保持相同布局)
- 禁止:提前返回或条件布局
错误处理
必须使用 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 | 仅用于主题偏好等少量场景 |
核心原则
- 懒加载所有重型内容:路由、DataGrid、图表、编辑器
- 用 Suspense 处理加载:使用 SuspenseLoader,而非提前返回
- useSuspenseQuery:新代码的主要数据获取模式
- 功能模块规范组织:api/、components/、hooks/、helpers/ 子目录
- 样式按大小决定:少于100行内联,超过100行独立文件
- 导入别名:使用 @/、~types、~components、~features
- 禁止提前返回:防止布局偏移
- useMuiSnackbar:所有用户通知统一使用