react-best-practices
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseReact Best Practices
React最佳实践
Comprehensive guide for building modern React and Next.js applications. Covers performance optimization, component architecture, shadcn/ui patterns, Motion animations, accessibility, and React 19+ features.
构建现代React和Next.js应用的全面指南。涵盖性能优化、组件架构、shadcn/ui模式、Motion动画、无障碍访问以及React 19+新特性。
When to Apply
适用场景
Reference these guidelines when:
- Writing new React components or Next.js pages
- Implementing data fetching (client or server-side)
- Building UI with shadcn/ui components
- Adding animations and micro-interactions
- Reviewing code for quality and performance
- Refactoring existing React/Next.js code
- Optimizing bundle size or load times
在以下场景中参考本指南:
- 编写新的React组件或Next.js页面
- 实现数据获取(客户端或服务端)
- 使用shadcn/ui组件构建UI
- 添加动画和微交互
- 评审代码以提升质量和性能
- 重构现有React/Next.js代码
- 优化包体积或加载时间
Rule Categories by Priority
按优先级划分的规则类别
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Component Architecture | CRITICAL | |
| 2 | Eliminating Waterfalls | CRITICAL | |
| 3 | Bundle Size Optimization | CRITICAL | |
| 4 | Server Components & Actions | HIGH | |
| 5 | shadcn/ui Patterns | HIGH | |
| 6 | State Management | MEDIUM-HIGH | |
| 7 | Motion & Animations | MEDIUM | |
| 8 | Re-render Optimization | MEDIUM | |
| 9 | Accessibility | MEDIUM | |
| 10 | TypeScript Patterns | MEDIUM | |
| 优先级 | 类别 | 影响程度 | 前缀 |
|---|---|---|---|
| 1 | 组件架构 | 关键 | |
| 2 | 消除请求瀑布 | 关键 | |
| 3 | 包体积优化 | 关键 | |
| 4 | 服务端组件与Actions | 高 | |
| 5 | shadcn/ui模式 | 高 | |
| 6 | 状态管理 | 中高 | |
| 7 | 动画与动效 | 中 | |
| 8 | 重渲染优化 | 中 | |
| 9 | 无障碍访问 | 中 | |
| 10 | TypeScript模式 | 中 | |
1. Component Architecture (CRITICAL)
1. 组件架构(关键)
Quick Reference
速查指南
- - Use functional components with hooks exclusively
arch-functional-components - - Build on existing components, don't extend
arch-composition-over-inheritance - - Each component should do one thing well
arch-single-responsibility - - Separate UI from logic when beneficial
arch-presentational-container - - Keep related files together (component, styles, tests)
arch-colocation - - Use Context or composition for deep props
arch-avoid-prop-drilling
- - 仅使用带Hooks的函数式组件
arch-functional-components - - 基于现有组件构建,不使用继承
arch-composition-over-inheritance - - 每个组件专注于单一功能
arch-single-responsibility - - 必要时分离UI与逻辑
arch-presentational-container - - 相关文件放在一起(组件、样式、测试)
arch-colocation - - 使用Context或组合方式避免深层属性传递
arch-avoid-prop-drilling
Key Principles
核心原则
Functional Components Only
typescript
// Correct: Functional component with hooks
function UserProfile({ userId }: { userId: string }) {
const { data: user } = useUser(userId)
return <div>{user?.name}</div>
}
// Incorrect: Class component
class UserProfile extends React.Component { /* ... */ }Composition Pattern
typescript
// Correct: Compose smaller components
function Card({ children }: { children: React.ReactNode }) {
return <div className="rounded-lg border p-4">{children}</div>
}
function CardHeader({ children }: { children: React.ReactNode }) {
return <div className="font-semibold">{children}</div>
}
// Usage
<Card>
<CardHeader>Title</CardHeader>
<p>Content</p>
</Card>Avoid Prop Drilling
typescript
// Incorrect: Passing props through many levels
<App user={user}>
<Layout user={user}>
<Sidebar user={user}>
<UserMenu user={user} />
</Sidebar>
</Layout>
</App>
// Correct: Use Context for shared state
const UserContext = createContext<User | null>(null)
function App() {
const user = useCurrentUser()
return (
<UserContext.Provider value={user}>
<Layout>
<Sidebar>
<UserMenu />
</Sidebar>
</Layout>
</UserContext.Provider>
)
}仅使用函数式组件
typescript
// Correct: Functional component with hooks
function UserProfile({ userId }: { userId: string }) {
const { data: user } = useUser(userId)
return <div>{user?.name}</div>
}
// Incorrect: Class component
class UserProfile extends React.Component { /* ... */ }组合模式
typescript
// Correct: Compose smaller components
function Card({ children }: { children: React.ReactNode }) {
return <div className="rounded-lg border p-4">{children}</div>
}
function CardHeader({ children }: { children: React.ReactNode }) {
return <div className="font-semibold">{children}</div>
}
// Usage
<Card>
<CardHeader>Title</CardHeader>
<p>Content</p>
</Card>避免属性透传
typescript
// Incorrect: Passing props through many levels
<App user={user}>
<Layout user={user}>
<Sidebar user={user}>
<UserMenu user={user} />
</Sidebar>
</Layout>
</App>
// Correct: Use Context for shared state
const UserContext = createContext<User | null>(null)
function App() {
const user = useCurrentUser()
return (
<UserContext.Provider value={user}>
<Layout>
<Sidebar>
<UserMenu />
</Sidebar>
</Layout>
</UserContext.Provider>
)
}2. Eliminating Waterfalls (CRITICAL)
2. 消除请求瀑布(关键)
Quick Reference
速查指南
- - Move await into branches where actually used
async-defer-await - - Use Promise.all() for independent operations
async-parallel - - Handle partial dependencies correctly
async-dependencies - - Start promises early, await late in API routes
async-api-routes - - Use Suspense to stream content
async-suspense-boundaries
- - 将await移至实际需要的分支中
async-defer-await - - 使用Promise.all()处理独立操作
async-parallel - - 正确处理部分依赖
async-dependencies - - 在API路由中尽早启动Promise,延迟await
async-api-routes - - 使用Suspense流式传输内容
async-suspense-boundaries
Key Principles
核心原则
Waterfalls are the #1 performance killer. Each sequential await adds full network latency.
Parallel Data Fetching
typescript
// Incorrect: Sequential waterfalls
async function Page() {
const user = await fetchUser()
const posts = await fetchPosts()
const comments = await fetchComments()
return <div>{/* render */}</div>
}
// Correct: Parallel fetching
async function Page() {
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
])
return <div>{/* render */}</div>
}Strategic Suspense Boundaries
typescript
// Stream content as it becomes available
function Page() {
return (
<div>
<Header />
<Suspense fallback={<PostsSkeleton />}>
<Posts />
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>
</div>
)
}请求瀑布是性能的头号杀手。每个顺序执行的await都会增加完整的网络延迟。
并行数据获取
typescript
// Incorrect: Sequential waterfalls
async function Page() {
const user = await fetchUser()
const posts = await fetchPosts()
const comments = await fetchComments()
return <div>{/* render */}</div>
}
// Correct: Parallel fetching
async function Page() {
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
])
return <div>{/* render */}</div>
}合理设置Suspense边界
typescript
// Stream content as it becomes available
function Page() {
return (
<div>
<Header />
<Suspense fallback={<PostsSkeleton />}>
<Posts />
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>
</div>
)
}3. Bundle Size Optimization (CRITICAL)
3. 包体积优化(关键)
Quick Reference
速查指南
- - Import directly, avoid barrel files
bundle-barrel-imports - - Use next/dynamic for heavy components
bundle-dynamic-imports - - Load analytics/logging after hydration
bundle-defer-third-party - - Load modules only when feature is activated
bundle-conditional - - Preload on hover/focus for perceived speed
bundle-preload
- - 直接导入,避免桶文件
bundle-barrel-imports - - 使用next/dynamic加载重型组件
bundle-dynamic-imports - - Hydration后再加载分析/日志工具
bundle-defer-third-party - - 仅在功能激活时加载模块
bundle-conditional - - 在hover/focus时预加载以提升感知速度
bundle-preload
Key Principles
核心原则
Avoid Barrel File Imports
typescript
// Incorrect: Imports entire library
import { Button } from '@/components'
import { formatDate } from '@/utils'
// Correct: Direct imports enable tree-shaking
import { Button } from '@/components/ui/button'
import { formatDate } from '@/utils/date'Dynamic Imports
typescript
import dynamic from 'next/dynamic'
// Load only when needed
const HeavyChart = dynamic(() => import('./HeavyChart'), {
loading: () => <ChartSkeleton />,
ssr: false
})
function Dashboard({ showChart }) {
return showChart ? <HeavyChart /> : null
}避免桶文件导入
typescript
// Incorrect: Imports entire library
import { Button } from '@/components'
import { formatDate } from '@/utils'
// Correct: Direct imports enable tree-shaking
import { Button } from '@/components/ui/button'
import { formatDate } from '@/utils/date'动态导入
typescript
import dynamic from 'next/dynamic'
// Load only when needed
const HeavyChart = dynamic(() => import('./HeavyChart'), {
loading: () => <ChartSkeleton />,
ssr: false
})
function Dashboard({ showChart }) {
return showChart ? <HeavyChart /> : null
}4. Server Components & Actions (HIGH)
4. 服务端组件与Actions(高)
Quick Reference
速查指南
- - Components are Server Components by default
server-default-server - - Add 'use client' only when needed
server-use-client-boundary - - Use Server Actions for mutations
server-actions - - Use React.cache() for per-request deduplication
server-cache-react - - Minimize data passed to client components
server-serialization
- - 组件默认是服务端组件
server-default-server - - 仅在需要时添加'use client'
server-use-client-boundary - - 使用Server Actions处理突变
server-actions - - 使用React.cache()实现请求级别的去重
server-cache-react - - 最小化传递给客户端组件的数据
server-serialization
Key Principles
核心原则
Server Components by Default
typescript
// Server Component (default) - can be async
async function ProductPage({ id }: { id: string }) {
const product = await db.product.findUnique({ where: { id } })
return <ProductDetails product={product} />
}
// Client Component - only when needed for interactivity
'use client'
function AddToCartButton({ productId }: { productId: string }) {
const [isPending, startTransition] = useTransition()
return (
<Button
onClick={() => startTransition(() => addToCart(productId))}
disabled={isPending}
>
Add to Cart
</Button>
)
}Server Actions
typescript
// actions.ts
'use server'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
await db.post.create({ data: { title, content } })
revalidatePath('/posts')
}
// Component usage
function CreatePostForm() {
return (
<form action={createPost}>
<Input name="title" placeholder="Title" />
<Textarea name="content" placeholder="Content" />
<Button type="submit">Create Post</Button>
</form>
)
}默认使用服务端组件
typescript
// Server Component (default) - can be async
async function ProductPage({ id }: { id: string }) {
const product = await db.product.findUnique({ where: { id } })
return <ProductDetails product={product} />
}
// Client Component - only when needed for interactivity
'use client'
function AddToCartButton({ productId }: { productId: string }) {
const [isPending, startTransition] = useTransition()
return (
<Button
onClick={() => startTransition(() => addToCart(productId))}
disabled={isPending}
>
Add to Cart
</Button>
)
}Server Actions
typescript
// actions.ts
'use server'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
await db.post.create({ data: { title, content } })
revalidatePath('/posts')
}
// Component usage
function CreatePostForm() {
return (
<form action={createPost}>
<Input name="title" placeholder="Title" />
<Textarea name="content" placeholder="Content" />
<Button type="submit">Create Post</Button>
</form>
)
}5. shadcn/ui Patterns (HIGH)
5. shadcn/ui模式(高)
Quick Reference
速查指南
- - Build on existing shadcn/ui primitives
shadcn-composition - - Use class-variance-authority for component variants
shadcn-variants - - Use CSS custom properties for theming
shadcn-theme-integration - - Leverage built-in accessibility from Radix
shadcn-accessibility - - Modify copied components, don't wrap excessively
shadcn-customization
- - 基于现有shadcn/ui原语构建
shadcn-composition - - 使用class-variance-authority实现组件变体
shadcn-variants - - 使用CSS自定义属性实现主题化
shadcn-theme-integration - - 利用Radix内置的无障碍特性
shadcn-accessibility - - 修改复制的组件,避免过度包装
shadcn-customization
Core Principles
核心原则
shadcn/ui is built around:
- Open Code: Components are copied into your project, fully customizable
- Composition: Every component uses a common, composable interface
- Beautiful Defaults: Carefully chosen default styles
- Accessibility by Default: Built on Radix UI primitives
shadcn/ui基于以下理念构建:
- 开放代码:组件会复制到你的项目中,可完全自定义
- 组合:每个组件使用通用的可组合接口
- 美观默认值:精心挑选的默认样式
- 默认无障碍:基于Radix UI原语构建
Component Installation
组件安装
bash
undefinedbash
undefinedAdd components as needed
Add components as needed
npx shadcn@latest add button
npx shadcn@latest add card
npx shadcn@latest add dialog
npx shadcn@latest add form
undefinednpx shadcn@latest add button
npx shadcn@latest add card
npx shadcn@latest add dialog
npx shadcn@latest add form
undefinedBuilding Custom Components
构建自定义组件
Composition Over Creation
typescript
// Correct: Build on existing primitives
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
interface ProductCardProps {
product: Product
onSelect?: () => void
}
function ProductCard({ product, onSelect }: ProductCardProps) {
return (
<Card
className="cursor-pointer hover:shadow-md transition-shadow"
onClick={onSelect}
>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>{product.name}</CardTitle>
{product.isNew && <Badge>New</Badge>}
</div>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">{product.description}</p>
<p className="text-lg font-bold mt-2">${product.price}</p>
</CardContent>
</Card>
)
}Using Variants with CVA
typescript
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const statusBadgeVariants = cva(
'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold',
{
variants: {
status: {
pending: 'bg-yellow-100 text-yellow-800',
active: 'bg-green-100 text-green-800',
inactive: 'bg-gray-100 text-gray-800',
error: 'bg-red-100 text-red-800',
},
},
defaultVariants: {
status: 'pending',
},
}
)
interface StatusBadgeProps
extends React.HTMLAttributes<HTMLSpanElement>,
VariantProps<typeof statusBadgeVariants> {
label: string
}
function StatusBadge({ status, label, className, ...props }: StatusBadgeProps) {
return (
<span className={cn(statusBadgeVariants({ status }), className)} {...props}>
{label}
</span>
)
}优先组合而非创建
typescript
// Correct: Build on existing primitives
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
interface ProductCardProps {
product: Product
onSelect?: () => void
}
function ProductCard({ product, onSelect }: ProductCardProps) {
return (
<Card
className="cursor-pointer hover:shadow-md transition-shadow"
onClick={onSelect}
>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>{product.name}</CardTitle>
{product.isNew && <Badge>New</Badge>}
</div>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">{product.description}</p>
<p className="text-lg font-bold mt-2">${product.price}</p>
</CardContent>
</Card>
)
}使用CVA实现变体
typescript
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const statusBadgeVariants = cva(
'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold',
{
variants: {
status: {
pending: 'bg-yellow-100 text-yellow-800',
active: 'bg-green-100 text-green-800',
inactive: 'bg-gray-100 text-gray-800',
error: 'bg-red-100 text-red-800',
},
},
defaultVariants: {
status: 'pending',
},
}
)
interface StatusBadgeProps
extends React.HTMLAttributes<HTMLSpanElement>,
VariantProps<typeof statusBadgeVariants> {
label: string
}
function StatusBadge({ status, label, className, ...props }: StatusBadgeProps) {
return (
<span className={cn(statusBadgeVariants({ status }), className)} {...props}>
{label}
</span>
)
}Common shadcn/ui Components
常见shadcn/ui组件
Forms with React Hook Form + Zod
typescript
'use client'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import * as z from 'zod'
import { Button } from '@/components/ui/button'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
const formSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
})
function LoginForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
email: '',
password: '',
},
})
function onSubmit(values: z.infer<typeof formSchema>) {
console.log(values)
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="you@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">Sign In</Button>
</form>
</Form>
)
}Dialog/Modal Pattern
typescript
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
function ConfirmDialog({
onConfirm,
title,
description
}: {
onConfirm: () => void
title: string
description: string
}) {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive">Delete</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline">Cancel</Button>
<Button variant="destructive" onClick={onConfirm}>
Confirm
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}Data Table with Tanstack Table
typescript
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import {
ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
} from '@tanstack/react-table'
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]
data: TData[]
}
function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})
return (
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
)
}结合React Hook Form + Zod的表单
typescript
'use client'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import * as z from 'zod'
import { Button } from '@/components/ui/button'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
const formSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
})
function LoginForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
email: '',
password: '',
},
})
function onSubmit(values: z.infer<typeof formSchema>) {
console.log(values)
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="you@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">Sign In</Button>
</form>
</Form>
)
}对话框/模态框模式
typescript
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
function ConfirmDialog({
onConfirm,
title,
description
}: {
onConfirm: () => void
title: string
description: string
}) {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive">Delete</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline">Cancel</Button>
<Button variant="destructive" onClick={onConfirm}>
Confirm
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}结合Tanstack Table的数据表格
typescript
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import {
ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
} from '@tanstack/react-table'
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]
data: TData[]
}
function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})
return (
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
)
}6. State Management (MEDIUM-HIGH)
6. 状态管理(中高)
Quick Reference
速查指南
- - Use useState/useReducer for local state
state-local-first - - Use Context for infrequently changing data
state-context-static - - Compute derived values, don't store them
state-derived-compute - - Use URL for shareable/bookmarkable state
state-url-state - - Use SWR/TanStack Query for server state
state-server-state
- - 使用useState/useReducer管理本地状态
state-local-first - - 使用Context管理不频繁变化的数据
state-context-static - - 计算派生值,而非存储
state-derived-compute - - 使用URL存储可分享/可书签的状态
state-url-state - - 使用SWR/TanStack Query管理服务端状态
state-server-state
Key Principles
核心原则
Avoid Derived State
typescript
// Incorrect: Storing derived state
function ProductList({ products }) {
const [items, setItems] = useState(products)
const [count, setCount] = useState(products.length) // Derived!
// Bug: count can get out of sync with items
}
// Correct: Compute derived values
function ProductList({ products }) {
const [items, setItems] = useState(products)
const count = items.length // Always in sync
}URL State for Filters/Pagination
typescript
'use client'
import { useSearchParams, useRouter } from 'next/navigation'
function ProductFilters() {
const searchParams = useSearchParams()
const router = useRouter()
const category = searchParams.get('category') || 'all'
function setCategory(newCategory: string) {
const params = new URLSearchParams(searchParams)
params.set('category', newCategory)
router.push(`?${params.toString()}`)
}
return (
<Select value={category} onValueChange={setCategory}>
{/* options */}
</Select>
)
}避免存储派生状态
typescript
// Incorrect: Storing derived state
function ProductList({ products }) {
const [items, setItems] = useState(products)
const [count, setCount] = useState(products.length) // Derived!
// Bug: count can get out of sync with items
}
// Correct: Compute derived values
function ProductList({ products }) {
const [items, setItems] = useState(products)
const count = items.length // Always in sync
}使用URL存储筛选/分页状态
typescript
'use client'
import { useSearchParams, useRouter } from 'next/navigation'
function ProductFilters() {
const searchParams = useSearchParams()
const router = useRouter()
const category = searchParams.get('category') || 'all'
function setCategory(newCategory: string) {
const params = new URLSearchParams(searchParams)
params.set('category', newCategory)
router.push(`?${params.toString()}`)
}
return (
<Select value={category} onValueChange={setCategory}>
{/* options */}
</Select>
)
}7. Motion & Animations (MEDIUM)
7. 动画与动效(中)
Quick Reference
速查指南
- - Animations should enhance UX, not distract
motion-purposeful - - Use transform/opacity, avoid layout triggers
motion-performance - - Respect prefers-reduced-motion
motion-reduced-motion - - Use layoutId for shared element transitions
motion-layout-id - - Use AnimatePresence for exit animations
motion-exit-animations - - Define reusable animation states
motion-variants
- - 动画应增强用户体验,而非分散注意力
motion-purposeful - - 使用transform/opacity,避免触发布局变化
motion-performance - - 尊重prefers-reduced-motion设置
motion-reduced-motion - - 使用layoutId实现共享元素过渡
motion-layout-id - - 使用AnimatePresence实现退出动画
motion-exit-animations - - 定义可复用的动画状态
motion-variants
Installation
安装
bash
npm install motionbash
npm install motionCore Principles
核心原则
Motion (formerly Framer Motion) provides declarative animations that enhance user experience.
Basic Animations
typescript
'use client'
import { motion } from 'motion/react'
function FadeInCard({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="rounded-lg border p-4"
>
{children}
</motion.div>
)
}Interaction States
typescript
function InteractiveButton({ children }: { children: React.ReactNode }) {
return (
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
className="px-4 py-2 bg-primary text-primary-foreground rounded-md"
>
{children}
</motion.button>
)
}Exit Animations with AnimatePresence
typescript
import { motion, AnimatePresence } from 'motion/react'
function NotificationList({ notifications }: { notifications: Notification[] }) {
return (
<AnimatePresence>
{notifications.map((notification) => (
<motion.div
key={notification.id}
initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -50 }}
transition={{ duration: 0.2 }}
>
<Notification data={notification} />
</motion.div>
))}
</AnimatePresence>
)
}Shared Element Transitions
typescript
function ProductGrid({ products }: { products: Product[] }) {
const [selected, setSelected] = useState<Product | null>(null)
return (
<>
<div className="grid grid-cols-3 gap-4">
{products.map((product) => (
<motion.div
key={product.id}
layoutId={`product-${product.id}`}
onClick={() => setSelected(product)}
className="cursor-pointer"
>
<img src={product.image} alt={product.name} />
</motion.div>
))}
</div>
<AnimatePresence>
{selected && (
<motion.div
layoutId={`product-${selected.id}`}
className="fixed inset-0 flex items-center justify-center"
>
<ProductDetail product={selected} onClose={() => setSelected(null)} />
</motion.div>
)}
</AnimatePresence>
</>
)
}Reusable Variants
typescript
const fadeInUp = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -20 },
}
const staggerContainer = {
animate: {
transition: {
staggerChildren: 0.1,
},
},
}
function StaggeredList({ items }: { items: string[] }) {
return (
<motion.ul variants={staggerContainer} initial="initial" animate="animate">
{items.map((item, i) => (
<motion.li key={i} variants={fadeInUp}>
{item}
</motion.li>
))}
</motion.ul>
)
}Scroll-Triggered Animations
typescript
import { motion, useInView } from 'motion/react'
import { useRef } from 'react'
function ScrollReveal({ children }: { children: React.ReactNode }) {
const ref = useRef(null)
const isInView = useInView(ref, { once: true, margin: '-100px' })
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 50 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 50 }}
transition={{ duration: 0.5 }}
>
{children}
</motion.div>
)
}Respecting Reduced Motion
typescript
import { motion, useReducedMotion } from 'motion/react'
function AccessibleAnimation({ children }: { children: React.ReactNode }) {
const shouldReduceMotion = useReducedMotion()
return (
<motion.div
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: shouldReduceMotion ? 0 : 0.3 }}
>
{children}
</motion.div>
)
}Motion(原Framer Motion)提供声明式动画,可提升用户体验。
基础动画
typescript
'use client'
import { motion } from 'motion/react'
function FadeInCard({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="rounded-lg border p-4"
>
{children}
</motion.div>
)
}交互状态
typescript
function InteractiveButton({ children }: { children: React.ReactNode }) {
return (
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
className="px-4 py-2 bg-primary text-primary-foreground rounded-md"
>
{children}
</motion.button>
)
}使用AnimatePresence实现退出动画
typescript
import { motion, AnimatePresence } from 'motion/react'
function NotificationList({ notifications }: { notifications: Notification[] }) {
return (
<AnimatePresence>
{notifications.map((notification) => (
<motion.div
key={notification.id}
initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -50 }}
transition={{ duration: 0.2 }}
>
<Notification data={notification} />
</motion.div>
))}
</AnimatePresence>
)
}共享元素过渡
typescript
function ProductGrid({ products }: { products: Product[] }) {
const [selected, setSelected] = useState<Product | null>(null)
return (
<>
<div className="grid grid-cols-3 gap-4">
{products.map((product) => (
<motion.div
key={product.id}
layoutId={`product-${product.id}`}
onClick={() => setSelected(product)}
className="cursor-pointer"
>
<img src={product.image} alt={product.name} />
</motion.div>
))}
</div>
<AnimatePresence>
{selected && (
<motion.div
layoutId={`product-${selected.id}`}
className="fixed inset-0 flex items-center justify-center"
>
<ProductDetail product={selected} onClose={() => setSelected(null)} />
</motion.div>
)}
</AnimatePresence>
</>
)
}可复用变体
typescript
const fadeInUp = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -20 },
}
const staggerContainer = {
animate: {
transition: {
staggerChildren: 0.1,
},
},
}
function StaggeredList({ items }: { items: string[] }) {
return (
<motion.ul variants={staggerContainer} initial="initial" animate="animate">
{items.map((item, i) => (
<motion.li key={i} variants={fadeInUp}>
{item}
</motion.li>
))}
</motion.ul>
)
}滚动触发动画
typescript
import { motion, useInView } from 'motion/react'
import { useRef } from 'react'
function ScrollReveal({ children }: { children: React.ReactNode }) {
const ref = useRef(null)
const isInView = useInView(ref, { once: true, margin: '-100px' })
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 50 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 50 }}
transition={{ duration: 0.5 }}
>
{children}
</motion.div>
)
}尊重减少动画的设置
typescript
import { motion, useReducedMotion } from 'motion/react'
function AccessibleAnimation({ children }: { children: React.ReactNode }) {
const shouldReduceMotion = useReducedMotion()
return (
<motion.div
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: shouldReduceMotion ? 0 : 0.3 }}
>
{children}
</motion.div>
)
}Performance Tips
性能提示
- Use and
transformfor smooth 60fps animationsopacity - Set prop for complex animations
willChange - Keep exit animations short (under 300ms)
- Use to lazy-load animations
useInView - Avoid animating ,
width,height,topdirectlyleft
- 使用和
transform实现流畅的60fps动画opacity - 为复杂动画设置属性
willChange - 退出动画保持简短(300ms以内)
- 使用懒加载动画
useInView - 避免直接动画、
width、height、topleft
8. Re-render Optimization (MEDIUM)
8. 重渲染优化(中)
Quick Reference
速查指南
- - Extract expensive work into memoized components
rerender-memo - - Memoize expensive calculations
rerender-usememo - - Stabilize callback references
rerender-usecallback - - Use primitive dependencies in effects
rerender-dependencies - - Use startTransition for non-urgent updates
rerender-transitions
- - 将昂贵的逻辑提取到记忆化组件中
rerender-memo - - 记忆化昂贵的计算
rerender-usememo - - 稳定回调引用
rerender-usecallback - - 在effect中使用原始类型依赖
rerender-dependencies - - 使用startTransition处理非紧急更新
rerender-transitions
Key Principles
核心原则
Memoization for Expensive Components
typescript
import { memo } from 'react'
const ExpensiveList = memo(function ExpensiveList({
items
}: {
items: Item[]
}) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{/* expensive render */}</li>
))}
</ul>
)
})Stable Callbacks
typescript
function Parent() {
const [count, setCount] = useState(0)
// Stable reference - won't cause child re-renders
const handleClick = useCallback(() => {
setCount(c => c + 1)
}, [])
return <MemoizedChild onClick={handleClick} />
}Non-Urgent Updates with Transitions
typescript
function SearchResults() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [isPending, startTransition] = useTransition()
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setQuery(e.target.value) // Urgent: update input immediately
startTransition(() => {
setResults(filterResults(e.target.value)) // Non-urgent: can be interrupted
})
}
return (
<>
<Input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<ResultsList results={results} />
</>
)
}昂贵组件的记忆化
typescript
import { memo } from 'react'
const ExpensiveList = memo(function ExpensiveList({
items
}: {
items: Item[]
}) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{/* expensive render */}</li>
))}
</ul>
)
})稳定回调
typescript
function Parent() {
const [count, setCount] = useState(0)
// Stable reference - won't cause child re-renders
const handleClick = useCallback(() => {
setCount(c => c + 1)
}, [])
return <MemoizedChild onClick={handleClick} />
}使用Transition处理非紧急更新
typescript
function SearchResults() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [isPending, startTransition] = useTransition()
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setQuery(e.target.value) // Urgent: update input immediately
startTransition(() => {
setResults(filterResults(e.target.value)) // Non-urgent: can be interrupted
})
}
return (
<>
<Input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<ResultsList results={results} />
</>
)
}9. Accessibility (MEDIUM)
9. 无障碍访问(中)
Quick Reference
速查指南
- - Use correct HTML elements
a11y-semantic-html - - Ensure keyboard navigability
a11y-keyboard-nav - - Add descriptive labels for screen readers
a11y-aria-labels - - Manage focus in modals and dynamic content
a11y-focus-management - - Ensure sufficient color contrast
a11y-color-contrast
- - 使用正确的HTML元素
a11y-semantic-html - - 确保可通过键盘导航
a11y-keyboard-nav - - 为屏幕阅读器添加描述性标签
a11y-aria-labels - - 在模态框和动态内容中管理焦点
a11y-focus-management - - 确保足够的颜色对比度
a11y-color-contrast
Key Principles
核心原则
Semantic HTML
typescript
// Incorrect: div soup
<div onClick={handleClick}>Click me</div>
// Correct: semantic button
<button onClick={handleClick}>Click me</button>Focus Management in Modals
typescript
function Modal({ isOpen, onClose, children }) {
const closeButtonRef = useRef<HTMLButtonElement>(null)
useEffect(() => {
if (isOpen) {
closeButtonRef.current?.focus()
}
}, [isOpen])
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent>
{children}
<Button ref={closeButtonRef} onClick={onClose}>
Close
</Button>
</DialogContent>
</Dialog>
)
}Skip Links
typescript
function Layout({ children }) {
return (
<>
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4"
>
Skip to main content
</a>
<Header />
<main id="main-content">{children}</main>
</>
)
}语义化HTML
typescript
// Incorrect: div soup
<div onClick={handleClick}>Click me</div>
// Correct: semantic button
<button onClick={handleClick}>Click me</button>模态框中的焦点管理
typescript
function Modal({ isOpen, onClose, children }) {
const closeButtonRef = useRef<HTMLButtonElement>(null)
useEffect(() => {
if (isOpen) {
closeButtonRef.current?.focus()
}
}, [isOpen])
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent>
{children}
<Button ref={closeButtonRef} onClick={onClose}>
Close
</Button>
</DialogContent>
</Dialog>
)
}跳转链接
typescript
function Layout({ children }) {
return (
<>
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4"
>
Skip to main content
</a>
<Header />
<main id="main-content">{children}</main>
</>
)
}10. TypeScript Patterns (MEDIUM)
10. TypeScript模式(中)
Quick Reference
速查指南
- - Enable strict TypeScript configuration
ts-strict-mode - - Define explicit prop interfaces
ts-component-props - - Use generics for reusable components
ts-generics - - Use for state machines
ts-discriminated-unions - - Let TypeScript infer when obvious
ts-infer-when-possible
- - 启用严格的TypeScript配置
ts-strict-mode - - 定义明确的属性接口
ts-component-props - - 使用泛型构建可复用组件
ts-generics - - 用于状态机
ts-discriminated-unions - - 明显的情况下让TypeScript自动推断
ts-infer-when-possible
Key Principles
核心原则
Component Props
typescript
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'default' | 'destructive' | 'outline'
size?: 'sm' | 'md' | 'lg'
isLoading?: boolean
}
function Button({
variant = 'default',
size = 'md',
isLoading,
children,
disabled,
...props
}: ButtonProps) {
return (
<button
disabled={disabled || isLoading}
{...props}
>
{isLoading ? <Spinner /> : children}
</button>
)
}Discriminated Unions for State
typescript
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error }
function useAsync<T>(asyncFn: () => Promise<T>) {
const [state, setState] = useState<AsyncState<T>>({ status: 'idle' })
// TypeScript knows exact shape based on status
if (state.status === 'success') {
return state.data // TypeScript knows data exists
}
}Generic Components
typescript
interface SelectProps<T> {
options: T[]
value: T
onChange: (value: T) => void
getLabel: (option: T) => string
getValue: (option: T) => string
}
function Select<T>({ options, value, onChange, getLabel, getValue }: SelectProps<T>) {
return (
<select
value={getValue(value)}
onChange={(e) => {
const selected = options.find(o => getValue(o) === e.target.value)
if (selected) onChange(selected)
}}
>
{options.map((option) => (
<option key={getValue(option)} value={getValue(option)}>
{getLabel(option)}
</option>
))}
</select>
)
}组件属性
typescript
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'default' | 'destructive' | 'outline'
size?: 'sm' | 'md' | 'lg'
isLoading?: boolean
}
function Button({
variant = 'default',
size = 'md',
isLoading,
children,
disabled,
...props
}: ButtonProps) {
return (
<button
disabled={disabled || isLoading}
{...props}
>
{isLoading ? <Spinner /> : children}
</button>
)
}用于状态的判别联合
typescript
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error }
function useAsync<T>(asyncFn: () => Promise<T>) {
const [state, setState] = useState<AsyncState<T>>({ status: 'idle' })
// TypeScript knows exact shape based on status
if (state.status === 'success') {
return state.data // TypeScript knows data exists
}
}泛型组件
typescript
interface SelectProps<T> {
options: T[]
value: T
onChange: (value: T) => void
getLabel: (option: T) => string
getValue: (option: T) => string
}
function Select<T>({ options, value, onChange, getLabel, getValue }: SelectProps<T>) {
return (
<select
value={getValue(value)}
onChange={(e) => {
const selected = options.find(o => getValue(o) === e.target.value)
if (selected) onChange(selected)
}}
>
{options.map((option) => (
<option key={getValue(option)} value={getValue(option)}>
{getLabel(option)}
</option>
))}
</select>
)
}React 19+ Features
React 19+新特性
New Hooks
新Hooks
useActionState - Form state management
typescript
'use client'
import { useActionState } from 'react'
function SubscribeForm() {
const [state, formAction, isPending] = useActionState(
async (prevState, formData) => {
const email = formData.get('email')
const result = await subscribe(email)
return result
},
null
)
return (
<form action={formAction}>
<Input name="email" type="email" />
<Button type="submit" disabled={isPending}>
{isPending ? 'Subscribing...' : 'Subscribe'}
</Button>
{state?.error && <p className="text-red-500">{state.error}</p>}
</form>
)
}useOptimistic - Optimistic UI updates
typescript
'use client'
import { useOptimistic } from 'react'
function TodoList({ todos }: { todos: Todo[] }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo: Todo) => [...state, newTodo]
)
async function addTodo(formData: FormData) {
const title = formData.get('title') as string
const newTodo = { id: crypto.randomUUID(), title, completed: false }
addOptimisticTodo(newTodo) // Immediately show in UI
await createTodo(title) // Then persist to server
}
return (
<>
<form action={addTodo}>
<Input name="title" />
<Button type="submit">Add</Button>
</form>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</>
)
}use - Async resource reading
typescript
import { use, Suspense } from 'react'
async function fetchUser(id: string): Promise<User> {
const res = await fetch(`/api/users/${id}`)
return res.json()
}
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise) // Suspends until resolved
return <div>{user.name}</div>
}
function Page({ userId }: { userId: string }) {
const userPromise = fetchUser(userId)
return (
<Suspense fallback={<UserSkeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
)
}useActionState - 表单状态管理
typescript
'use client'
import { useActionState } from 'react'
function SubscribeForm() {
const [state, formAction, isPending] = useActionState(
async (prevState, formData) => {
const email = formData.get('email')
const result = await subscribe(email)
return result
},
null
)
return (
<form action={formAction}>
<Input name="email" type="email" />
<Button type="submit" disabled={isPending}>
{isPending ? 'Subscribing...' : 'Subscribe'}
</Button>
{state?.error && <p className="text-red-500">{state.error}</p>}
</form>
)
}useOptimistic - 乐观UI更新
typescript
'use client'
import { useOptimistic } from 'react'
function TodoList({ todos }: { todos: Todo[] }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo: Todo) => [...state, newTodo]
)
async function addTodo(formData: FormData) {
const title = formData.get('title') as string
const newTodo = { id: crypto.randomUUID(), title, completed: false }
addOptimisticTodo(newTodo) // Immediately show in UI
await createTodo(title) // Then persist to server
}
return (
<>
<form action={addTodo}>
<Input name="title" />
<Button type="submit">Add</Button>
</form>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</>
)
}use - 异步资源读取
typescript
import { use, Suspense } from 'react'
async function fetchUser(id: string): Promise<User> {
const res = await fetch(`/api/users/${id}`)
return res.json()
}
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise) // Suspends until resolved
return <div>{user.name}</div>
}
function Page({ userId }: { userId: string }) {
const userPromise = fetchUser(userId)
return (
<Suspense fallback={<UserSkeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
)
}Project Structure
项目结构
src/
├── app/ # Next.js App Router
│ ├── layout.tsx
│ ├── page.tsx
│ └── (routes)/
├── components/
│ ├── ui/ # shadcn/ui components
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ └── ...
│ └── features/ # Feature-specific components
│ ├── auth/
│ └── dashboard/
├── hooks/ # Custom hooks
├── lib/ # Utilities
│ ├── utils.ts # cn() helper, etc.
│ └── validations.ts # Zod schemas
├── actions/ # Server Actions
└── types/ # TypeScript typessrc/
├── app/ # Next.js App Router
│ ├── layout.tsx
│ ├── page.tsx
│ └── (routes)/
├── components/
│ ├── ui/ # shadcn/ui components
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ └── ...
│ └── features/ # 功能特定组件
│ ├── auth/
│ └── dashboard/
├── hooks/ # 自定义Hooks
├── lib/ # 工具函数
│ ├── utils.ts # cn() 辅助函数等
│ └── validations.ts # Zod 模式
├── actions/ # Server Actions
└── types/ # TypeScript 类型