关于 react-typescript
react-typescript 面向使用 TypeScript 5.9 构建类型安全 React 19.2 应用的开发者,汇集新版 React 模式与严格类型实践,并说明如何借助 React Compiler 减少手动记忆化。
主要功能
- 01React 19 组件与 Hook 模式
- 02类型安全的属性与状态建模
- 03React Compiler 记忆化指南
- 04严格的 TypeScript 配置
- 05Activity 与 useEffectEvent 模式
适用场景
- 01使用原生元素属性创建 React 组件
- 02建模异步状态以排除无效组合
- 03构建可保留隐藏标签页状态的界面
- 04借助 React Compiler 减少手动记忆化
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ tenequm/ skills --skill react-typescript安装后在 Claude Code 中输入/react-typescript确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
LICENSE.txtreferences/react-19-features.mdtypescript-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>
)
}
最佳实践
- 组件使用普通函数——不要使用
FC、forwardRef或memo。旧版类型中的FC会隐式为children添加类型,而相比普通函数签名并无额外价值。让 React Compiler 进行优化。 - 使用
React.ComponentProps<"element">扩展原生元素——可涵盖所有 HTML 属性。 - 状态使用判别联合类型,而不是布尔值——在类型层面避免不可能的状态。
- 使用
use()而不是useContext()——支持有条件地调用;结合守卫处理上下文时也更简洁。 - 配置对象使用
satisfies——验证结构的同时保留字面量类型。 - 需要保留状态时(如标签页、侧边栏、向导)使用
Activity,而不是条件渲染。 - 使用
useEffectEvent,而不是抑制依赖项——从 effect 中清晰地提取非响应式逻辑。 - 使用严格的 tsconfig——启用
noUncheckedIndexedAccess、exactOptionalPropertyTypes和verbatimModuleSyntax。 - 路由配置、主题令牌和查找对象使用
as const satisfies。 - 在 switch 中缩窄类型——在 default 分支中通过
never执行穷尽性检查。 - 使用
import defer(TS 5.9)——将模块求值推迟到首次访问属性时,以便延迟加载大型模块。参见 typescript-patterns.md。
深入了解
- react-19-features.md - React 19/19.2 功能完整参考,包含详细示例
- typescript-patterns.md - React 的高级 TypeScript 模式:泛型、实用类型、严格配置、类型级编程
资源
- React 文档:https://react.dev
- React 19.2 博客文章:https://react.dev/blog/2025/10/01/react-19-2
- React Compiler:https://react.dev/learn/react-compiler
- TypeScript 5.9:https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-9.html
- TypeScript 5.8:https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-8.html
- React TypeScript 速查表:https://react-typescript-cheatsheet.netlify.app/