mcpField
R
react-best-practices

react-best-practices

github.com/dedalus-erp-pas开发257 次安装
代码仓库

关于 react-best-practices

面向 React 和 Next.js 开发者的实践指南,覆盖组件架构、性能、UI 模式、无障碍设计及 React 19+ 功能。

主要功能

  1. 01组件架构与组合模式
  2. 02异步瀑布与包体积优化
  3. 03服务端组件与 Actions
  4. 04shadcn/ui 和状态管理模式
  5. 05动画与无障碍指导
  6. 06TypeScript 和 React 19+ 模式

适用场景

  1. 01设计或重构 React 组件
  2. 02改进数据获取和包体积性能
  3. 03使用 shadcn/ui 构建界面
  4. 04检查无障碍与 TypeScript 用法

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dedalus-erp-pas/foundation-skills --skill react-best-practices

安装后在 Claude Code 中输入/react-best-practices确认已加载。

详细介绍

React 最佳实践

构建现代 React 和 Next.js 应用的综合指南。涵盖性能优化、组件架构、shadcn/ui 模式、Motion 动画、无障碍访问以及 React 19+ 功能。

适用场景

在以下情况下参考这些指南:

  • 编写新的 React 组件或 Next.js 页面
  • 实现数据获取(客户端或服务端)
  • 使用 shadcn/ui 组件构建 UI
  • 添加动画和微交互
  • 审查代码质量和性能
  • 重构现有 React/Next.js 代码
  • 优化打包体积或加载时间

按优先级划分的规则类别

优先级类别影响前缀
1组件架构严重arch-
2消除瀑布流严重async-
3打包体积优化严重bundle-
4服务端组件与操作高server-
5shadcn/ui 模式高shadcn-
6状态管理中高state-
7Motion 与动画中motion-
8重新渲染优化中rerender-
9无障碍访问中a11y-
10TypeScript 模式中ts-

1. 组件架构(严重)

快速参考
  • arch-functional-components - 只使用带钩子的函数组件
  • arch-composition-over-inheritance - 基于现有组件构建,不要扩展组件
  • arch-single-responsibility - 每个组件都应专注做好一件事
  • arch-presentational-container - 在适当情况下分离 UI 与逻辑
  • arch-colocation - 将相关文件放在一起(组件、样式、测试)
  • arch-avoid-prop-drilling - 使用 Context 或组合来处理深层属性传递
核心原则

仅使用函数组件

// 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 { /* ... */ }

组合模式

// 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>

避免属性层层传递

// 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. 消除瀑布流(严重)

快速参考
  • async-defer-await - 将 await 移到实际使用它的分支中
  • async-parallel - 对互相独立的操作使用 Promise.all()
  • async-dependencies - 正确处理部分依赖关系
  • async-api-routes - 在 API 路由中尽早启动 promise,延后等待
  • async-suspense-boundaries - 使用 Suspense 流式传输内容
核心原则

瀑布流是性能的头号杀手。每次顺序等待都会增加完整的网络延迟。

并行获取数据

// 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 边界

// Stream content as it becomes available
function Page() {
  return (
    <div>
      <Header />
      <Suspense fallback={<PostsSkeleton />}>
        <Posts />
      </Suspense>
      <Suspense fallback={<CommentsSkeleton />}>
        <Comments />
      </Suspense>
    </div>
  )
}

3. 打包体积优化(严重)

快速参考
  • bundle-barrel-imports - 直接导入,避免使用聚合导出文件
  • bundle-dynamic-imports - 对较大的组件使用 next/dynamic
  • bundle-defer-third-party - 在 hydration 后加载分析/日志功能
  • bundle-conditional - 仅在功能启用时加载模块
  • bundle-preload - 在悬停/聚焦时预加载,提升感知速度
核心原则

避免从聚合导出文件导入

// 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'

动态导入

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-default-server - 组件默认是服务端组件
  • server-use-client-boundary - 仅在需要时添加 'use client'
  • server-actions - 使用服务端操作执行变更
  • server-cache-react - 使用 React.cache() 在每次请求内去重
  • server-serialization - 尽量减少传递给客户端组件的数据
核心原则

默认使用服务端组件

// 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>
  )
}

服务端操作

// 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 模式(高)

快速参考
  • shadcn-composition - 基于现有 shadcn/ui 基元构建
  • shadcn-variants - 使用 class-variance-authority 定义组件变体
  • shadcn-theme-integration - 使用 CSS 自定义属性实现主题
  • shadcn-accessibility - 利用 Radix 内置的无障碍功能
  • shadcn-customization - 修改复制的组件,避免过度封装
核心原则

shadcn/ui 围绕以下原则构建:

  • 开放代码:组件会复制到你的项目中,可完全自定义
  • 组合:每个组件都使用通用且可组合的接口
  • 精美的默认样式:精心挑选的默认样式
  • 默认具备无障碍性:基于 Radix UI 基础组件构建
组件安装
# Add components as needed
npx shadcn@latest add button
npx shadcn@latest add card
npx shadcn@latest add dialog
npx shadcn@latest add form
构建自定义组件

组合优于创建

// 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 变体

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>
  )
}
常见的 shadcn/ui 组件

使用 React Hook Form + Zod 构建表单

'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="[email protected]" {...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>
  )
}

对话框/模态框模式

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 构建数据表

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-local-first - 局部状态使用 useState/useReducer
  • state-context-static - 对不常变化的数据使用 Context
  • state-derived-compute - 计算派生值,而不要存储它们
  • state-url-state - 使用 URL 管理可分享/可添加书签的状态
  • state-server-state - 使用 SWR/TanStack Query 管理服务端状态
核心原则

避免派生状态

// 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 管理筛选/分页状态

'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-purposeful - 动画应提升用户体验,而非分散注意力
  • motion-performance - 使用 transform/opacity,避免触发布局
  • motion-reduced-motion - 遵循 prefers-reduced-motion 设置
  • motion-layout-id - 使用 layoutId 实现共享元素过渡
  • motion-exit-animations - 使用 AnimatePresence 实现退出动画
  • motion-variants - 定义可复用的动画状态
安装
npm install motion
核心原则

Motion(前身为 Framer Motion)提供声明式动画,提升用户体验。

基础动画

'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>
  )
}

交互状态

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 实现退出动画

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>
  )
}

共享元素过渡

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>
    </>
  )
}

可复用的变体

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>
  )
}

滚动触发动画

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>
  )
}

遵循减少动态效果设置

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>
  )
}
性能提示
  • 使用 transform 和 opacity 实现流畅的 60fps 动画
  • 为复杂动画设置 willChange 属性
  • 退出动画应保持简短(低于 300ms)
  • 使用 useInView 延迟加载动画
  • 避免直接为 width、height、top、left 添加动画

8. 重渲染优化(中优先级)

快速参考
  • rerender-memo - 将耗费较多资源的工作提取到经过记忆化的组件中
  • rerender-usememo - 对耗费较多资源的计算进行记忆化
  • rerender-usecallback - 稳定回调引用
  • rerender-dependencies - 在 effect 中使用原始类型依赖项
  • rerender-transitions - 使用 startTransition 处理非紧急更新
核心原则

对开销较大的组件进行记忆化

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>
  )
})

稳定的回调

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 处理非紧急更新

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. 无障碍性(中优先级)

快速参考
  • a11y-semantic-html - 使用正确的 HTML 元素
  • a11y-keyboard-nav - 确保支持键盘导航
  • a11y-aria-labels - 为屏幕阅读器添加描述性标签
  • a11y-focus-management - 管理模态框和动态内容中的焦点
  • a11y-color-contrast - 确保足够的颜色对比度
核心原则

语义化 HTML

// Incorrect: div soup
<div onClick={handleClick}>Click me</div>

// Correct: semantic button
<button onClick={handleClick}>Click me</button>

管理模态框中的焦点

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>
  )
}

跳转链接

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 模式(中优先级)

快速参考
  • ts-strict-mode - 启用严格的 TypeScript 配置
  • ts-component-props - 明确定义属性接口
  • ts-generics - 使用泛型构建可复用组件
  • ts-discriminated-unions - 用于状态机
  • ts-infer-when-possible - 显而易见时让 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>
  )
}

使用可辨识联合管理状态

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
  }
}

泛型组件

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+ 功能

新增 Hook

useActionState - 表单状态管理

'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 更新

'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 - 异步资源读取

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 types

References

评论

支持 Markdown