mcpField
R
react-typescript

react-typescript

github.com/tenequm开发3981 次安装2 天前更新
代码仓库

关于 react-typescript

react-typescript 面向使用 TypeScript 5.9 构建类型安全 React 19.2 应用的开发者,汇集新版 React 模式与严格类型实践,并说明如何借助 React Compiler 减少手动记忆化。

主要功能

  1. 01React 19 组件与 Hook 模式
  2. 02类型安全的属性与状态建模
  3. 03React Compiler 记忆化指南
  4. 04严格的 TypeScript 配置
  5. 05Activity 与 useEffectEvent 模式

适用场景

  1. 01使用原生元素属性创建 React 组件
  2. 02建模异步状态以排除无效组合
  3. 03构建可保留隐藏标签页状态的界面
  4. 04借助 React Compiler 减少手动记忆化

安装

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

$npx skills add https://github.com/tenequm/skills --skill react-typescript

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

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

LICENSE.txt
references/
react-19-features.md
typescript-patterns.md

详细介绍

React TypeScript

使用 TypeScript 5.9 构建类型安全的 React 19.2 应用的模式。React Compiler 会自动处理记忆化——编写普通组件,让工具进行优化。

关键规则

不要使用 forwardRef——ref 现在是一个 Prop
// WRONG - deprecated pattern
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => (
  <input ref={ref} {...props} />
))

// CORRECT - React 19: ref is a regular prop
function Input({ ref, ...props }: React.ComponentProps<"input">) {
  return <input ref={ref} {...props} />
}
使用 React Compiler 时不要手动进行记忆化
// WRONG - unnecessary with React Compiler
const MemoizedList = memo(function List({ items }: { items: Item[] }) {
  const sorted = useMemo(() => items.toSorted(compare), [items])
  const handleClick = useCallback((id: string) => onSelect(id), [onSelect])
  return sorted.map(item => <Row key={item.id} onClick={() => handleClick(item.id)} />)
})

// CORRECT - React Compiler auto-memoizes all of this
function List({ items, onSelect }: { items: Item[]; onSelect: (id: string) => void }) {
  const sorted = items.toSorted(compare)
  return sorted.map(item => <Row key={item.id} onClick={() => onSelect(item.id)} />)
}
使用 React.ComponentProps<> 获取元素的 Props
// WRONG - manual HTML attribute typing
interface ButtonProps {
  onClick?: (e: MouseEvent<HTMLButtonElement>) => void
  disabled?: boolean
  children: React.ReactNode
  className?: string
}

// CORRECT - extend native element props
type ButtonProps = React.ComponentProps<"button"> & {
  variant?: "primary" | "ghost"
}
使用带判别的联合类型描述状态,而不是布尔值
// WRONG - impossible states possible
interface RequestState { isLoading: boolean; error: string | null; data: User | null }

// CORRECT - discriminated union prevents impossible states
type RequestState =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "error"; error: string }
  | { status: "success"; data: User }
使用 satisfies 确保字面量的类型安全
// WRONG - widens to Record<string, Route>
const routes: Record<string, Route> = { home: { path: "/" }, about: { path: "/about" } }

// CORRECT - preserves literal keys while checking shape
const routes = {
  home: { path: "/" },
  about: { path: "/about" },
} satisfies Record<string, Route>

routes.home // typed, autocomplete works
Context 必须有严格的默认值,否则就抛出异常
// WRONG - null default with no guard
const AuthContext = createContext<AuthState | null>(null)
// consumers must null-check every time

// CORRECT - factory hook that throws on missing provider
const AuthContext = createContext<AuthState | null>(null)

function useAuth(): AuthState {
  const ctx = use(AuthContext)
  if (ctx === null) throw new Error("useAuth must be used within AuthProvider")
  return ctx
}
优先使用 use(),而不是 useContext()
// OLD pattern
function Header() {
  const theme = useContext(ThemeContext) // cannot use after early return
  if (!isVisible) return null
  return <h1 style={{ color: theme.color }}>Title</h1>
}

// CORRECT - React 19: use() works after early returns
function Header({ isVisible }: { isVisible: boolean }) {
  if (!isVisible) return null
  const theme = use(ThemeContext) // works here - use() is not bound by hook rules
  return <h1 style={{ color: theme.color }}>Title</h1>
}

React 19 模式

编写组件

使用带 data-slot 的普通函数作为样式钩子。不使用 forwardRef,也不使用 FC:

type CardProps = React.ComponentProps<"div"> & {
  variant?: "elevated" | "outlined"
}

function Card({ variant = "outlined", className, ...props }: CardProps) {
  return (
    <div
      data-slot="card"
      data-variant={variant}
      className={cn("rounded-xl border bg-card", className)}
      {...props}
    />
  )
}

function CardTitle({ className, ...props }: React.ComponentProps<"h3">) {
  return <h3 data-slot="card-title" className={cn("font-semibold", className)} {...props} />
}
Actions 和 useTransition

转换中的异步函数会自动处理待处理状态、错误和表单重置:

function UpdateProfile({ userId }: { userId: string }) {
  const [error, submitAction, isPending] = useActionState(
    async (_prev: string | null, formData: FormData) => {
      const result = await updateProfile(userId, formData)
      if (result.error) return result.error
      redirect("/profile")
      return null
    },
    null
  )

  return (
    <form action={submitAction}>
      <input type="text" name="displayName" required />
      <button type="submit" disabled={isPending}>
        {isPending ? "Saving..." : "Save"}
      </button>
      {error && <p className="text-destructive">{error}</p>}
    </form>
  )
}

用于非表单 Actions 的 useTransition:

function DeleteButton({ onDelete }: { onDelete: () => Promise<void> }) {
  const [isPending, startTransition] = useTransition()

  return (
    <button
      disabled={isPending}
      onClick={() => startTransition(async () => { await onDelete() })}
    >
      {isPending ? "Deleting..." : "Delete"}
    </button>
  )
}

使用 useOptimistic 提供即时反馈:

function LikeButton({ likes, onLike }: { likes: number; onLike: () => Promise<void> }) {
  const [optimisticLikes, addOptimisticLike] = useOptimistic(likes, (prev) => prev + 1)

  const handleLike = async () => {
    addOptimisticLike(null)
    await onLike()
  }

  return (
    <form action={handleLike}>
      <button type="submit">{optimisticLikes} Likes</button>
    </form>
  )
}
use() Hook

在渲染过程中读取 Promise 和 Context。可以有条件地调用,也可以在提前返回之后调用:

// Reading a promise - suspends until resolved
function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
  const comments = use(commentsPromise)
  return (
    <ul>
      {comments.map(c => <li key={c.id}>{c.text}</li>)}
    </ul>
  )
}

// Parent gets promise from loader/cache, NOT created during render
function PostPage({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
  return (
    <Suspense fallback={<Skeleton />}>
      <Comments commentsPromise={commentsPromise} />
    </Suspense>
  )
}

// Reading context conditionally
function AdminPanel({ user }: { user: User | null }) {
  if (!user) return <LoginPrompt />
  const permissions = use(PermissionsContext) // legal - use() works after early return
  if (!permissions.isAdmin) return <Forbidden />
  return <Dashboard user={user} permissions={permissions} />
}

重要: use() 不支持在渲染过程中创建的 Promise。请传入来自加载器、服务端函数或缓存来源的 Promise。

Activity 组件(React 19.2)

保留隐藏 UI 的状态。隐藏的子组件会保留状态和 DOM,但副作用会卸载:

import { Activity, useState } from "react"

function TabLayout({ tabs }: { tabs: TabConfig[] }) {
  const [activeTab, setActiveTab] = useState(tabs[0].id)

  return (
    <div>
      <nav>
        {tabs.map(tab => (
          <button key={tab.id} onClick={() => setActiveTab(tab.id)}>
            {tab.label}
          </button>
        ))}
      </nav>

      {tabs.map(tab => (
        <Activity key={tab.id} mode={activeTab === tab.id ? "visible" : "hidden"}>
          <tab.component />
        </Activity>
      ))}
    </div>
  )
}

主要行为:

  • visible - 正常渲染,副作用已挂载
  • hidden - 通过 display: none 隐藏,副作用会清理,状态保留,更新会延后
  • 预渲染:<Activity mode="hidden"> 会以低优先级渲染子组件,以便之后更快地显示
  • DOM 副作用(视频、音频)在隐藏时仍会持续运行——请添加 useLayoutEffect 清理逻辑
useEffectEvent(React 19.2)

从副作用中提取非响应式逻辑。事件函数始终能读取最新的 props/state,且不会触发副作用重新运行:

function ChatRoom({ roomId, theme }: { roomId: string; theme: string }) {
  const onConnected = useEffectEvent(() => {
    showNotification("Connected!", theme) // always reads latest theme
  })

  useEffect(() => {
    const connection = createConnection(roomId)
    connection.on("connected", () => onConnected())
    connection.connect()
    return () => connection.disconnect()
  }, [roomId]) // theme NOT in deps - onConnected is an Effect Event
}

规则:

  • 只能在副作用或其他副作用事件内部调用
  • 不要传递给子组件,也不要将其包含在依赖项数组中
  • 不要在渲染期间调用
  • 用于概念上属于由副作用触发的“事件”的逻辑

自定义 Hook 模式:

function useInterval(callback: () => void, delay: number | null) {
  const onTick = useEffectEvent(callback)

  useEffect(() => {
    if (delay === null) return
    const id = setInterval(() => onTick(), delay)
    return () => clearInterval(id)
  }, [delay])
}
文档元数据

直接在组件中渲染 <title>、<meta> 和 <link>——React 会将它们提升到 <head> 中:

function BlogPost({ post }: { post: Post }) {
  return (
    <article>
      <title>{post.title}</title>
      <meta name="description" content={post.excerpt} />
      <meta name="author" content={post.author} />
      <link rel="canonical" href={`https://example.com/posts/${post.slug}`} />
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  )
}
将 Context 用作 Provider
const ThemeContext = createContext<Theme>("light")

// React 19 - use Context directly as provider (no .Provider)
function App({ children }: { children: React.ReactNode }) {
  return (
    <ThemeContext value="dark">
      {children}
    </ThemeContext>
  )
}
Ref 清理函数
function MeasuredBox() {
  return (
    <div
      ref={(node) => {
        if (node) {
          const observer = new ResizeObserver(handleResize)
          observer.observe(node)
          return () => observer.disconnect() // cleanup on unmount
        }
      }}
    />
  )
}

React Compiler

功能

React Compiler(babel-plugin-react-compiler)会在构建时分析代码并自动插入记忆化。在大多数情况下,它会取代手动使用的 useMemo、useCallback 和 React.memo。

自动记忆化:

  • 组件返回值(当 props 未更改时跳过重新渲染)
  • 组件中的开销较大的计算
  • 作为 props 传递的回调函数
  • JSX 元素的创建
设置(Vite)
pnpm add -D babel-plugin-react-compiler
// vite.config.ts
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: ["babel-plugin-react-compiler"], // must be first
      },
    }),
  ],
})
不要做什么
// DON'T - compiler handles this
const MemoComponent = memo(MyComponent)
const memoized = useMemo(() => expensive(data), [data])
const stableCallback = useCallback(() => handler(id), [id])

// DO - write plain code, compiler optimizes
function MyComponent({ data, onSelect }: Props) {
  const processed = expensive(data)
  return <Child onClick={() => onSelect(data.id)} />
}
仍适用手动记忆化的情况
  • 将 useMemo/useCallback 用作副作用依赖项,以便精确控制副作用的触发时机
  • 在多个组件间共享的值(编译器按组件进行记忆化,而非全局记忆化)
  • 选择退出:"use no memo" 指令会跳过对特定组件的编译
验证

经编译器优化的组件会在 React DevTools 中显示“Memo”徽标。检查构建输出中是否有 react/compiler-runtime 导入。

TypeScript 模式

React 严格 tsconfig
{
  "compilerOptions": {
    "strict": true,
    "target": "esnext",
    "module": "nodenext",
    "moduleDetection": "force",
    "jsx": "react-jsx",
    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noUncheckedSideEffectImports": true,
    "skipLibCheck": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "types": []
  }
}
组件 Props 模式
// Extending native element props
type ButtonProps = React.ComponentProps<"button"> & {
  variant?: "primary" | "secondary"
  isLoading?: boolean
}

function Button({ variant = "primary", isLoading, children, ...props }: ButtonProps) {
  return (
    <button data-slot="button" disabled={isLoading || props.disabled} {...props}>
      {isLoading ? <Spinner /> : children}
    </button>
  )
}

// Polymorphic "as" prop
type PolymorphicProps<E extends React.ElementType> = {
  as?: E
} & Omit<React.ComponentProps<E>, "as">

function Text<E extends React.ElementType = "span">({
  as,
  ...props
}: PolymorphicProps<E>) {
  const Component = as || "span"
  return <Component {...props} />
}

// Usage: <Text as="h1">Hello</Text>
事件处理程序类型
function Form() {
  // Inferred from handler context - no explicit typing needed
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    const formData = new FormData(e.currentTarget)
    // process formData
  }

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value)
  }

  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === "Enter") submit()
  }

  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleChange} onKeyDown={handleKeyDown} />
    </form>
  )
}
Hook 类型
// useState - inferred when initial value provided
const [count, setCount] = useState(0) // number
const [user, setUser] = useState<User | null>(null) // explicit for null init

// useReducer - discriminated union actions
type CounterAction =
  | { type: "increment"; amount: number }
  | { type: "decrement"; amount: number }
  | { type: "reset" }

function counterReducer(state: number, action: CounterAction): number {
  switch (action.type) {
    case "increment": return state + action.amount
    case "decrement": return state - action.amount
    case "reset": return 0
  }
}

const [count, dispatch] = useReducer(counterReducer, 0)
dispatch({ type: "increment", amount: 5 })

// useRef - element refs (React 19: returns RefObject<T | null>, always nullable)
const inputRef = useRef<HTMLInputElement>(null)

// useRef - mutable value (no null)
const intervalRef = useRef<number | undefined>(undefined)
泛型组件
type SelectProps<T> = {
  items: T[]
  value: T
  onChange: (item: T) => void
  getLabel: (item: T) => string
  getKey: (item: T) => string
}

function Select<T>({ items, value, onChange, getLabel, getKey }: SelectProps<T>) {
  return (
    <select
      value={getKey(value)}
      onChange={(e) => {
        const item = items.find(i => getKey(i) === e.target.value)
        if (item) onChange(item)
      }}
    >
      {items.map(item => (
        <option key={getKey(item)} value={getKey(item)}>
          {getLabel(item)}
        </option>
      ))}
    </select>
  )
}

// Usage - T inferred as User
<Select
  items={users}
  value={selectedUser}
  onChange={setSelectedUser}
  getLabel={u => u.name}
  getKey={u => u.id}
/>
使用判别联合描述组件状态
type AsyncState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "error"; error: Error }
  | { status: "success"; data: T }

function AsyncContent<T>({
  state,
  render,
}: {
  state: AsyncState<T>
  render: (data: T) => React.ReactNode
}) {
  switch (state.status) {
    case "idle": return null
    case "loading": return <Spinner />
    case "error": return <ErrorDisplay error={state.error} />
    case "success": return <>{render(state.data)}</>
  }
}
Zod v4 集成
import { z } from "zod"

const UserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  role: z.enum(["admin", "user", "viewer"]),
})

type User = z.infer<typeof UserSchema>

// Form validation with useActionState
type FieldErrors = { name?: string[]; email?: string[]; role?: string[] }

function CreateUser() {
  const [errors, submitAction, isPending] = useActionState(
    async (_prev: FieldErrors | null, formData: FormData) => {
      const result = UserSchema.safeParse(Object.fromEntries(formData))
      if (!result.success) {
        // Zod v4: use z.flattenError() to get field-level errors
        const flat = z.flattenError(result.error)
        return flat.fieldErrors as FieldErrors
      }
      await saveUser(result.data)
      return null
    },
    null
  )

  return (
    <form action={submitAction}>
      <input name="name" />
      {errors?.name && (
        <span className="text-destructive">{errors.name[0]}</span>
      )}
      <input name="email" type="email" />
      <select name="role">
        <option value="user">User</option>
        <option value="admin">Admin</option>
        <option value="viewer">Viewer</option>
      </select>
      <button type="submit" disabled={isPending}>Create</button>
    </form>
  )
}

最佳实践

  1. 组件使用普通函数——不要使用 FC、forwardRef 或 memo。旧版类型中的 FC 会隐式为 children 添加类型,而相比普通函数签名并无额外价值。让 React Compiler 进行优化。
  2. 使用 React.ComponentProps<"element"> 扩展原生元素——可涵盖所有 HTML 属性。
  3. 状态使用判别联合类型,而不是布尔值——在类型层面避免不可能的状态。
  4. 使用 use() 而不是 useContext()——支持有条件地调用;结合守卫处理上下文时也更简洁。
  5. 配置对象使用 satisfies——验证结构的同时保留字面量类型。
  6. 需要保留状态时(如标签页、侧边栏、向导)使用 Activity,而不是条件渲染。
  7. 使用 useEffectEvent,而不是抑制依赖项——从 effect 中清晰地提取非响应式逻辑。
  8. 使用严格的 tsconfig——启用 noUncheckedIndexedAccess、exactOptionalPropertyTypes 和 verbatimModuleSyntax。
  9. 路由配置、主题令牌和查找对象使用 as const satisfies。
  10. 在 switch 中缩窄类型——在 default 分支中通过 never 执行穷尽性检查。
  11. 使用 import defer(TS 5.9)——将模块求值推迟到首次访问属性时,以便延迟加载大型模块。参见 typescript-patterns.md。

深入了解

资源

评论

支持 Markdown