React + TypeScript Agent 开发规范 (Development Rules)
文章目录
- [React + TypeScript Agent 开发规范 (Development Rules)](#React + TypeScript Agent 开发规范 (Development Rules))
-
-
- [1. 项目架构与文件组织](#1. 项目架构与文件组织)
- [2. 类型安全规范 (Strict TypeScript)](#2. 类型安全规范 (Strict TypeScript))
- [3. 组件开发规范](#3. 组件开发规范)
- [4. Hooks 开发规范](#4. Hooks 开发规范)
- [5. 状态管理规范](#5. 状态管理规范)
- [6. 数据获取规范 (TanStack Query)](#6. 数据获取规范 (TanStack Query))
- [7. 表单处理规范 (React Hook Form + Zod)](#7. 表单处理规范 (React Hook Form + Zod))
- [8. 性能优化规范](#8. 性能优化规范)
- [9. 错误处理规范](#9. 错误处理规范)
- [10. 测试规范](#10. 测试规范)
- [11. ESLint & 代码质量配置](#11. ESLint & 代码质量配置)
- [12. Agent 指令模板](#12. Agent 指令模板)
-

1. 项目架构与文件组织
typescript
// 强制目录结构
src/
├── components/ // 纯展示组件 (Presentational)
│ ├── ui/ // 原子级基础组件 (Button, Input, Card)
│ ├── forms/ // 表单相关组件
│ └── layout/ // 布局组件 (Header, Sidebar, Footer)
├── features/ // 功能模块 (按业务域划分)
│ ├── auth/
│ │ ├── components/ // 该功能专用组件
│ │ ├── hooks/ // 该功能专用 Hooks
│ │ ├── services/ // API 调用
│ │ ├── types/ // 领域类型定义
│ │ └── utils/ // 工具函数
│ └── dashboard/
├── hooks/ // 全局通用 Hooks
├── lib/ // 第三方库配置 (axios, queryClient)
├── providers/ // React Context Providers
├── routes/ // 路由配置
├── stores/ // 状态管理 (Zustand/Redux)
├── styles/ // 全局样式、主题配置
├── types/ // 全局类型定义
├── utils/ // 纯工具函数 (无 React 依赖)
└── App.tsx
Agent 必须遵守:
- 组件文件使用 PascalCase:
UserProfile.tsx - 工具文件使用 camelCase:
formatDate.ts - 类型文件使用
.types.ts后缀:user.types.ts - 每个文件夹必须包含
index.ts进行统一导出
2. 类型安全规范 (Strict TypeScript)
typescript
// ✅ 必须:启用严格模式 (tsconfig.json)
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true
}
}
// ✅ 必须:明确定义组件 Props 类型
interface UserCardProps {
user: User;
onEdit?: (id: string) => void;
variant?: 'default' | 'compact';
}
// ❌ 禁止:使用 any
function processData(data: any) { ... }
// ✅ 必须:使用 unknown 并类型收窄
function processData(data: unknown) {
if (isUser(data)) {
// 类型安全操作
}
}
// ✅ 必须:使用 satisfies 进行类型验证
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000
} satisfies AppConfig;
// ✅ 必须:泛型组件明确定义约束
interface ListProps<T extends { id: string }> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
3. 组件开发规范
typescript
// ✅ 必须:使用函数组件 + Hooks,禁止 Class 组件
// ✅ 必须:默认导出组件,Props 作为命名导出类型
// components/ui/Button.tsx
import { forwardRef, type ComponentPropsWithoutRef } from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/utils/cn';
// 1. 定义样式变体
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground',
outline: 'border border-input bg-background hover:bg-accent',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline'
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
}
);
// 2. 定义 Props 类型(必须继承原生属性)
export interface ButtonProps
extends ComponentPropsWithoutRef<'button'>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
loading?: boolean;
}
// 3. 使用 forwardRef 支持 ref 转发
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, loading, disabled, children, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
disabled={disabled || loading}
{...props}
>
{loading && <Spinner className="mr-2 h-4 w-4 animate-spin" />}
{children}
</Comp>
);
}
);
Button.displayName = 'Button';
// 4. 统一导出
export { buttonVariants };
组件开发 checklist:
- 使用
forwardRef支持 ref 转发 - Props 类型继承原生 HTML 属性 (
ComponentPropsWithoutRef) - 使用
displayName便于调试 - 支持
className合并(使用cn工具函数) - 加载状态、禁用状态处理
- 无障碍属性 (aria-label, role 等)
4. Hooks 开发规范
typescript
// ✅ 必须:自定义 Hook 以 use 开头
// ✅ 必须:返回类型使用 as const 或明确接口
// hooks/useAsync.ts
import { useState, useCallback, useRef, useEffect } from 'react';
interface UseAsyncOptions<T> {
immediate?: boolean;
onSuccess?: (data: T) => void;
onError?: (error: Error) => void;
}
interface UseAsyncReturn<T, Args extends unknown[]> {
execute: (...args: Args) => Promise<T | undefined>;
data: T | null;
error: Error | null;
isLoading: boolean;
isSuccess: boolean;
isError: boolean;
reset: () => void;
}
export function useAsync<T, Args extends unknown[] = []>(
asyncFunction: (...args: Args) => Promise<T>,
options: UseAsyncOptions<T> = {}
): UseAsyncReturn<T, Args> {
const { immediate = false, onSuccess, onError } = options;
const [state, setState] = useState<{
data: T | null;
error: Error | null;
isLoading: boolean;
status: 'idle' | 'pending' | 'success' | 'error';
}>({
data: null,
error: null,
isLoading: false,
status: 'idle'
});
// 使用 ref 避免闭包问题
const asyncFunctionRef = useRef(asyncFunction);
const isMountedRef = useRef(true);
useEffect(() => {
asyncFunctionRef.current = asyncFunction;
}, [asyncFunction]);
useEffect(() => {
return () => {
isMountedRef.current = false;
};
}, []);
const execute = useCallback(async (...args: Args): Promise<T | undefined> => {
setState(prev => ({ ...prev, isLoading: true, status: 'pending' }));
try {
const data = await asyncFunctionRef.current(...args);
if (isMountedRef.current) {
setState({ data, error: null, isLoading: false, status: 'success' });
onSuccess?.(data);
}
return data;
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
if (isMountedRef.current) {
setState({ data: null, error: err, isLoading: false, status: 'error' });
onError?.(err);
}
throw err;
}
}, [onSuccess, onError]);
const reset = useCallback(() => {
setState({ data: null, error: null, isLoading: false, status: 'idle' });
}, []);
// 立即执行逻辑
useEffect(() => {
if (immediate) {
execute();
}
}, [immediate, execute]);
return {
execute,
data: state.data,
error: state.error,
isLoading: state.isLoading,
isSuccess: state.status === 'success',
isError: state.status === 'error',
reset
} as const;
}
// ✅ 必须:提供使用示例和文档注释
/**
* 用于管理异步操作的 Hook
* @example
* const { data, isLoading, execute } = useAsync(fetchUser, { immediate: true });
*
* // 手动触发
* const handleSubmit = async (id: string) => {
* const result = await execute(id);
* };
*/
Hooks 开发 checklist:
- 处理组件卸载时的内存泄漏
- 使用
useRef解决闭包陈旧问题 - 返回稳定的引用(使用
useCallback/useMemo) - 提供完整的类型定义和 JSDoc
- 支持取消/重置操作
5. 状态管理规范
typescript
// ✅ 推荐:使用 Zustand 进行状态管理
// stores/userStore.ts
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
interface User {
id: string;
email: string;
name: string;
role: 'admin' | 'user';
}
interface UserState {
// State
user: User | null;
isAuthenticated: boolean;
preferences: {
theme: 'light' | 'dark' | 'system';
language: string;
};
// Computed (使用 selector 模式)
isAdmin: () => boolean;
// Actions
setUser: (user: User | null) => void;
updatePreferences: (prefs: Partial<UserState['preferences']>) => void;
logout: () => void;
}
export const useUserStore = create<UserState>()(
devtools(
persist(
immer((set, get) => ({
// Initial state
user: null,
isAuthenticated: false,
preferences: {
theme: 'system',
language: 'zh-CN'
},
// Computed
isAdmin: () => get().user?.role === 'admin',
// Actions
setUser: (user) => set((state) => {
state.user = user;
state.isAuthenticated = !!user;
}),
updatePreferences: (prefs) => set((state) => {
Object.assign(state.preferences, prefs);
}),
logout: () => set((state) => {
state.user = null;
state.isAuthenticated = false;
})
})),
{
name: 'user-storage',
partialize: (state) => ({
preferences: state.preferences
// 不持久化敏感信息
})
}
),
{ name: 'UserStore' }
)
);
// ✅ 必须:使用 Selector 优化重渲染
// 组件中使用
function UserProfile() {
// ❌ 错误:订阅整个 store
const { user } = useUserStore();
// ✅ 正确:只订阅需要的字段
const userName = useUserStore(state => state.user?.name);
// ✅ 正确:使用 selector 进行复杂计算
const isAdmin = useUserStore(state => state.isAdmin());
// ✅ 正确:使用 shallow 比较数组/对象
const preferences = useUserStore(
state => state.preferences,
shallow
);
}
6. 数据获取规范 (TanStack Query)
typescript
// ✅ 必须:使用 TanStack Query 进行服务端状态管理
// features/users/api/queries.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { userApi } from './api';
import type { User, CreateUserInput, UpdateUserInput } from './types';
// Query Keys 工厂模式
export const userKeys = {
all: ['users'] as const,
lists: () => [...userKeys.all, 'list'] as const,
list: (filters: UserFilters) => [...userKeys.lists(), filters] as const,
details: () => [...userKeys.all, 'detail'] as const,
detail: (id: string) => [...userKeys.details(), id] as const,
};
// 获取用户列表
export function useUsers(filters: UserFilters) {
return useQuery({
queryKey: userKeys.list(filters),
queryFn: () => userApi.getUsers(filters),
staleTime: 5 * 60 * 1000, // 5分钟
gcTime: 10 * 60 * 1000, // 10分钟
select: (data) => ({
...data,
users: data.users.map(normalizeUser) // 数据转换
})
});
}
// 获取单个用户
export function useUser(id: string) {
return useQuery({
queryKey: userKeys.detail(id),
queryFn: () => userApi.getUser(id),
enabled: !!id, // 条件查询
retry: (failureCount, error) => {
if (error.status === 404) return false; // 404 不重试
return failureCount < 3;
}
});
}
// 创建用户
export function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (input: CreateUserInput) => userApi.createUser(input),
onSuccess: (newUser) => {
// 乐观更新或重新获取
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
queryClient.setQueryData(userKeys.detail(newUser.id), newUser);
},
onError: (error) => {
toast.error(`创建失败: ${error.message}`);
}
});
}
// 组件中使用
function UserList() {
const [filters, setFilters] = useState({ page: 1, search: '' });
const { data, isLoading, error, isFetching } = useUsers(filters);
if (isLoading) return <Skeleton />;
if (error) return <ErrorBoundary error={error} />;
return (
<div>
{isFetching && <LoadingOverlay />}
<UserTable data={data.users} />
</div>
);
}
7. 表单处理规范 (React Hook Form + Zod)
typescript
// ✅ 必须:使用 React Hook Form + Zod 进行表单验证
// features/auth/components/LoginForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// 1. 定义 Schema
const loginSchema = z.object({
email: z.string().email('请输入有效的邮箱地址'),
password: z.string().min(8, '密码至少需要8位'),
rememberMe: z.boolean().default(false)
});
type LoginFormData = z.infer<typeof loginSchema>;
export function LoginForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting, isDirty, isValid },
setError,
reset
} = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
mode: 'onBlur', // 验证时机
defaultValues: {
email: '',
password: '',
rememberMe: false
}
});
const onSubmit = async (data: LoginFormData) => {
try {
await authApi.login(data);
reset();
} catch (error) {
// 设置表单级错误
if (error.code === 'INVALID_CREDENTIALS') {
setError('root', {
message: '邮箱或密码错误'
});
}
}
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div>
<Label htmlFor="email">邮箱</Label>
<Input
id="email"
type="email"
{...register('email')}
aria-invalid={!!errors.email}
/>
{errors.email && (
<ErrorMessage>{errors.email.message}</ErrorMessage>
)}
</div>
<div>
<Label htmlFor="password">密码</Label>
<Input
id="password"
type="password"
{...register('password')}
/>
{errors.password && (
<ErrorMessage>{errors.password.message}</ErrorMessage>
)}
</div>
<div className="flex items-center space-x-2">
<Checkbox id="rememberMe" {...register('rememberMe')} />
<Label htmlFor="rememberMe">记住我</Label>
</div>
{errors.root && (
<Alert variant="destructive">{errors.root.message}</Alert>
)}
<Button
type="submit"
disabled={!isDirty || !isValid || isSubmitting}
loading={isSubmitting}
>
登录
</Button>
</form>
);
}
8. 性能优化规范
typescript
// ✅ 必须:使用 React.memo 优化纯展示组件
interface ExpensiveListProps {
items: Item[];
onItemClick: (id: string) => void;
}
export const ExpensiveList = React.memo<ExpensiveListProps>(
function ExpensiveList({ items, onItemClick }) {
return (
<ul>
{items.map(item => (
<ListItem
key={item.id}
item={item}
onClick={onItemClick}
/>
))}
</ul>
);
},
(prevProps, nextProps) => {
// 自定义比较函数
return (
prevProps.items === nextProps.items &&
prevProps.onItemClick === nextProps.onItemClick
);
}
);
// ✅ 必须:使用 useMemo 缓存计算
function Dashboard({ data }: { data: DataPoint[] }) {
const stats = useMemo(() => {
return data.reduce((acc, point) => ({
total: acc.total + point.value,
avg: acc.avg + point.value / data.length,
max: Math.max(acc.max, point.value)
}), { total: 0, avg: 0, max: 0 });
}, [data]);
// ✅ 必须:使用 useCallback 缓存回调
const handleRefresh = useCallback(() => {
refetch();
}, [refetch]);
return <DashboardView stats={stats} onRefresh={handleRefresh} />;
}
// ✅ 必须:代码分割和懒加载
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<HeavyComponent />
</Suspense>
);
}
// ✅ 必须:虚拟列表处理大数据
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }: { items: Item[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
overscan: 5
});
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`
}}
>
{items[virtualItem.index].name}
</div>
))}
</div>
</div>
);
}
9. 错误处理规范
typescript
// ✅ 必须:使用 Error Boundary 捕获渲染错误
// components/ErrorBoundary.tsx
import { Component, type ErrorInfo, type ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
}
interface State {
hasError: boolean;
error: Error | null;
}
export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false, error: null };
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
this.props.onError?.(error, errorInfo);
// 上报到监控服务
reportError(error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback || <DefaultErrorFallback error={this.state.error} />;
}
return this.props.children;
}
}
// ✅ 必须:API 错误统一处理
// lib/api.ts
import axios, { AxiosError } from 'axios';
class ApiError extends Error {
constructor(
message: string,
public status: number,
public code: string,
public data?: unknown
) {
super(message);
this.name = 'ApiError';
}
}
api.interceptors.response.use(
(response) => response.data,
(error: AxiosError) => {
if (error.response) {
throw new ApiError(
error.response.data?.message || '请求失败',
error.response.status,
error.response.data?.code || 'UNKNOWN_ERROR',
error.response.data
);
}
throw new ApiError('网络错误', 0, 'NETWORK_ERROR');
}
);
// ✅ 必须:使用 react-error-boundary 处理异步错误
import { ErrorBoundary } from 'react-error-boundary';
function QueryErrorBoundary({ children }: { children: ReactNode }) {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// 重置 QueryClient
queryClient.clear();
}}
>
{children}
</ErrorBoundary>
);
}
10. 测试规范
typescript
// ✅ 必须:组件使用 Testing Library + Vitest
// Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { Button } from './Button';
describe('Button', () => {
it('renders correctly', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
});
it('handles click events', () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('is disabled when loading', () => {
render(<Button loading>Loading</Button>);
expect(screen.getByRole('button')).toBeDisabled();
expect(screen.getByRole('status')).toBeInTheDocument(); // spinner
});
it('forwards ref correctly', () => {
const ref = { current: null as HTMLButtonElement | null };
render(<Button ref={ref}>Click me</Button>);
expect(ref.current).toBeInstanceOf(HTMLButtonElement);
});
});
// ✅ 必须:Hooks 使用 renderHook 测试
// hooks/useAsync.test.ts
import { renderHook, waitFor } from '@testing-library/react';
import { useAsync } from './useAsync';
describe('useAsync', () => {
it('handles successful execution', async () => {
const mockFn = vi.fn().mockResolvedValue('data');
const { result } = renderHook(() => useAsync(mockFn));
expect(result.current.isLoading).toBe(false);
act(() => {
result.current.execute();
});
expect(result.current.isLoading).toBe(true);
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
expect(result.current.data).toBe('data');
});
});
});
11. ESLint & 代码质量配置
json
// .eslintrc.json
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended-type-checked",
"plugin:@typescript-eslint/stylistic-type-checked",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:jsx-a11y/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json"
},
"plugins": ["@typescript-eslint", "react", "react-refresh"],
"rules": {
"react-refresh/only-export-components": "warn",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/strict-boolean-expressions": "error",
"no-console": ["warn", { "allow": ["error", "warn"] }]
}
}
12. Agent 指令模板
当你作为 Agent 生成代码时,必须遵循以下流程:
markdown
## Agent 代码生成指令
### 前置检查
1. 检查项目是否已存在相关功能,避免重复
2. 确认技术栈版本(React 18+, TypeScript 5+)
3. 检查是否有可用的 UI 组件库
### 代码生成步骤
1. **类型优先**:先定义 Props 和返回类型
2. **逻辑分离**:将业务逻辑提取到 Hooks
3. **组件实现**:使用函数组件 + forwardRef
4. **样式处理**:使用 Tailwind CSS + cn 工具函数
5. **错误处理**:添加 Error Boundary 和 loading 状态
6. **性能优化**:检查是否需要 memo/useMemo/useCallback
7. **导出配置**:添加 index.ts 统一导出
### 禁止事项
- ❌ 使用 any 类型
- ❌ 在组件内定义函数(除非使用 useCallback)
- ❌ 直接修改 state(必须使用不可变更新)
- ❌ 在 useEffect 中直接使用 async 函数
- ❌ 忽略 accessibility 属性
- ❌ 硬编码字符串(使用 i18n)
- ❌ 在 render 中执行副作用
### 必须包含
- [ ] TypeScript 严格类型
- [ ] JSDoc 注释
- [ ] 使用示例
- [ ] 错误处理
- [ ] 单元测试(复杂逻辑)
- [ ] Storybook 文档(UI 组件)
这套规范涵盖了现代 React + TypeScript 开发的核心最佳实践,可以作为 .cursorrules、.claude-rules 或类似的 Agent 配置文件使用。