关于 react-19
面向编码助手的参考技能,说明 React 19 的变更、API 和迁移注意事项。内容聚焦于相较旧版 React 的差异,并非完整的 React 参考手册。
主要功能
- 01React 19 API 变更
- 02新 Hooks 和 Form Actions
- 03迁移指南与 codemods
- 04React 19.2 功能说明
- 05TypeScript 与弃用 API 说明
适用场景
- 01将应用升级到 React 19
- 02查阅 Hooks 和 ref 的变更
- 03替换已移除的 React API
- 04了解 React 19.2 的功能
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jbmanllr/ agent-skills --skill react-19安装后在 Claude Code 中输入/react-19确认已加载。
详细介绍
React 19 - 主要变更
本技能聚焦于 React 19 中发生了哪些变化,并非完整的 React 参考资料。
从 React 16/17 迁移?
如果从 18 之前的版本升级,这些变更已逐步累积,现在都是必需的:
| 变更 | 引入版本 | React 19 状态 |
|---|---|---|
createRoot / hydrateRoot | React 18 | 必需(已移除 ReactDOM.render) |
| 并发渲染 | React 18 | 所有 R19 功能的基础 |
| 自动批处理 | React 18 | 默认行为 |
useId、useSyncExternalStore | React 18 | 稳定,广泛使用 |
| Hooks(新代码不使用类组件) | React 16.8 | 新功能的唯一实现方式 |
createContext(非旧版 Context) | React 16.3 | 必需(已移除旧版 Context) |
| 错误边界 | React 16 | 现在提供更完善的错误回调 |
**迁移路径:**先升级到 React 18.3(它会显示弃用警告),然后再升级到 19。
React 19 的思维方式
React 19 从根本上改变了我们思考 React 的方式:
| 旧思维 | 新思维 |
|---|---|
| 默认采用客户端渲染 | 服务端优先(默认使用 RSC) |
| 手动记忆化 | 由编译器处理 |
使用 useEffect 获取数据 | 异步服务端组件 |
使用 useState 处理表单 | 表单 Actions |
| 布尔值表示加载状态 | Suspense 边界 |
| 优化所有内容 | 编写正确的代码,由编译器优化 |
有关思维模型的变化,请参阅 references/paradigm-shifts.md。
有关应停止采用的做法,请参阅 references/anti-patterns.md。
快速参考:新增内容
| 功能 | React 18 | React 19+ |
|---|---|---|
| 记忆化 | 手动(useMemo、useCallback、memo) | React Compiler(自动)或手动 |
| 转发 refs | forwardRef() 包装器 | 将 ref 作为普通属性 |
| Context 提供程序 | <Context.Provider value={}> | <Context value={}> |
| 表单状态 | 使用 useState 自行实现 | useActionState Hook |
| 乐观更新 | 手动管理状态 | useOptimistic Hook |
| 读取 Promise | 无法在渲染中读取 | use() Hook |
| 条件式 Context | 无法实现 | 在条件语句后使用 use(Context) |
| 表单待处理状态 | 手动跟踪 | useFormStatus Hook |
| Ref 清理 | 卸载时传入 null | 返回清理函数 |
| 文档元数据 | react-helmet 或手动处理 | 原生 <title>、<meta>、<link> |
| 使用状态隐藏/显示 UI | 卸载/重新挂载(状态会丢失) | <Activity> 组件(19.2+) |
| 非响应式 Effect 逻辑 | 添加到依赖项或禁用 lint | useEffectEvent Hook(19.2+) |
| 自定义元素 | 部分支持 | 完全支持(将 props 作为属性处理) |
| 水合错误 | 多条模糊错误 | 单条带差异对比的错误 |
React Compiler 与记忆化
启用 React Compiler 后,手动记忆化是可选的,并非禁止:
// React Compiler handles this automatically
function Component({ items }) {
const filtered = items.filter(x => x.active);
const sorted = filtered.sort((a, b) => a.name.localeCompare(b.name));
const handleClick = (id) => console.log(id);
return <List items={sorted} onClick={handleClick} />;
}
// Manual memoization still works as escape hatch for fine-grained control
const filtered = useMemo(() => expensiveOperation(items), [items]);
const handleClick = useCallback((id) => onClick(id), [onClick]);
在 React Compiler 中使用手动记忆化的场景:
- 需要稳定引用的 Effect 依赖项
- 在多个组件间共享开销较大的计算(编译器不会共享)
- 明确控制何时重新计算
详情请参阅 references/react-compiler.md。
将 ref 作为属性(已弃用 forwardRef)
// React 19: ref is just a prop
function Input({ placeholder, ref }) {
return <input placeholder={placeholder} ref={ref} />;
}
// Usage - no change
const inputRef = useRef(null);
<Input ref={inputRef} placeholder="Enter text" />
// forwardRef still works but will be deprecated
// Codemod: npx codemod@latest react/19/replace-forward-ref
Ref 清理函数
// React 19: Return cleanup function from ref callback
<input
ref={(node) => {
// Setup
node?.focus();
// Return cleanup (called on unmount or ref change)
return () => {
console.log('Cleanup');
};
}}
/>
// React 18: Received null on unmount (still works, but cleanup preferred)
<input ref={(node) => {
if (node) { /* setup */ }
else { /* cleanup */ }
}} />
将 Context 作为提供程序
const ThemeContext = createContext('light');
// React 19: Use Context directly
function App({ children }) {
return (
<ThemeContext value="dark">
{children}
</ThemeContext>
);
}
// React 18: Required .Provider (still works, will be deprecated)
<ThemeContext.Provider value="dark">
{children}
</ThemeContext.Provider>
新 Hooks
useActionState
import { useActionState } from 'react';
function Form() {
const [error, submitAction, isPending] = useActionState(
async (prevState, formData) => {
const result = await saveData(formData.get('name'));
if (result.error) return result.error;
redirect('/success');
return null;
},
null // initial state
);
return (
<form action={submitAction}>
<input name="name" disabled={isPending} />
<button disabled={isPending}>
{isPending ? 'Saving...' : 'Save'}
</button>
{error && <p className="error">{error}</p>}
</form>
);
}
useOptimistic
import { useOptimistic } from 'react';
function Messages({ messages, sendMessage }) {
const [optimisticMessages, addOptimistic] = useOptimistic(
messages,
(state, newMessage) => [...state, { ...newMessage, sending: true }]
);
async function handleSubmit(formData) {
const message = { text: formData.get('text'), id: Date.now() };
addOptimistic(message); // Show immediately
await sendMessage(message); // Reverts on error
}
return (
<form action={handleSubmit}>
{optimisticMessages.map(m => (
<div key={m.id} style={{ opacity: m.sending ? 0.5 : 1 }}>
{m.text}
</div>
))}
<input name="text" />
</form>
);
}
use() Hook
import { use, Suspense } from 'react';
// Read promises (suspends until resolved)
function Comments({ commentsPromise }) {
const comments = use(commentsPromise);
return comments.map(c => <p key={c.id}>{c.text}</p>);
}
// Usage with Suspense
<Suspense fallback={<Spinner />}>
<Comments commentsPromise={fetchComments()} />
</Suspense>
// Conditional context reading (not possible with useContext!)
function Theme({ showTheme }) {
if (!showTheme) return <div>Plain</div>;
const theme = use(ThemeContext); // Can be called conditionally!
return <div style={{ color: theme.primary }}>Themed</div>;
}
useFormStatus(react-dom)
import { useFormStatus } from 'react-dom';
// Must be used inside a <form> - reads parent form status
function SubmitButton() {
const { pending, data, method, action } = useFormStatus();
return (
<button disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
);
}
function Form() {
return (
<form action={serverAction}>
<input name="email" />
<SubmitButton /> {/* Reads form status via context */}
</form>
);
}
完整的 API 详情请参阅 references/new-hooks.md。
表单 Actions
// Pass function directly to form action
<form action={async (formData) => {
'use server';
await saveToDatabase(formData);
}}>
<input name="email" type="email" />
<button type="submit">Subscribe</button>
</form>
// Button-level actions
<form>
<button formAction={saveAction}>Save</button>
<button formAction={deleteAction}>Delete</button>
</form>
// Manual form reset
import { requestFormReset } from 'react-dom';
requestFormReset(formElement);
文档元数据
// Automatically hoisted to <head> - works in any component
function BlogPost({ post }) {
return (
<article>
<title>{post.title}</title>
<meta name="description" content={post.excerpt} />
<meta name="author" content={post.author} />
<link rel="canonical" href={post.url} />
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
资源预加载
import { prefetchDNS, preconnect, preload, preinit } from 'react-dom';
function App() {
// DNS prefetch
prefetchDNS('https://api.example.com');
// Establish connection early
preconnect('https://fonts.googleapis.com');
// Preload resources
preload('https://example.com/font.woff2', { as: 'font' });
preload('/hero.jpg', { as: 'image' });
// Load and execute script eagerly
preinit('https://example.com/analytics.js', { as: 'script' });
return <main>...</main>;
}
样式表支持
// precedence controls insertion order and deduplication
function Component() {
return (
<>
<link rel="stylesheet" href="/base.css" precedence="default" />
<link rel="stylesheet" href="/theme.css" precedence="high" />
<div className="styled">Content</div>
</>
);
}
// React ensures stylesheets load before Suspense boundary reveals
<Suspense fallback={<Skeleton />}>
<link rel="stylesheet" href="/feature.css" precedence="default" />
<FeatureComponent />
</Suspense>
自定义元素支持
React 19 全面支持自定义元素(Web Components)。
// Props matching element properties are assigned as properties
// Others are assigned as attributes
<my-element
stringAttr="hello" // Attribute (string)
complexProp={{ foo: 'bar' }} // Property (object)
onCustomEvent={handleEvent} // Property (function)
/>
**客户端:**React 会检查元素实例上是否存在某个属性。如果存在,则将其赋值为属性;否则,将其作为特性赋值。
**服务端(SSR):**基本类型(字符串、数字)会渲染为特性。对象、函数和符号会从 HTML 中省略。
// Define custom element
class MyElement extends HTMLElement {
constructor() {
super();
this.data = undefined; // React will assign to this property
}
connectedCallback() {
this.textContent = JSON.stringify(this.data);
}
}
customElements.define('my-element', MyElement);
// Use in React
<my-element data={{ items: [1, 2, 3] }} />
水合改进
更清晰的错误消息
React 19 会显示一条包含差异的错误,而不是多条含糊的错误:
Uncaught Error: Hydration failed because the server rendered HTML
didn't match the client.
<App>
<span>
+ Client
- Server
兼容第三方脚本
React 19 能妥善处理浏览器扩展或第三方脚本插入的元素:
- 跳过
<head>和<body>中意外出现的标签(不会出现不匹配错误) - 重新渲染期间会保留扩展添加的样式表
- 对扩展注入的内容,无需添加
suppressHydrationWarning
已移除的 API(破坏性变更)
| 已移除 | 迁移方式 |
|---|---|
ReactDOM.render() | createRoot().render() |
ReactDOM.hydrate() | hydrateRoot() |
unmountComponentAtNode() | root.unmount() |
ReactDOM.findDOMNode() | 使用 refs |
propTypes | 使用 TypeScript 或移除 |
defaultProps(函数) | ES6 默认参数 |
| 字符串 refs | 回调 refs 或 useRef |
| 旧版 Context | createContext |
React.createFactory | JSX |
react-dom/test-utils | 使用来自 'react' 的 act |
迁移指南请参阅 references/deprecations.md。
TypeScript 变更
// useRef requires argument
const ref = useRef<HTMLDivElement>(null); // Required
const ref = useRef(); // Error in React 19
// Ref callbacks must not return values (except cleanup)
<div ref={(node) => { instance = node; }} /> // Correct
<div ref={(node) => (instance = node)} /> // Error - implicit return
// ReactElement props are now unknown (not any)
type Props = ReactElement['props']; // unknown in R19, any in R18
// JSX namespace - import explicitly
import type { JSX } from 'react';
codemod 请参阅 references/typescript-changes.md。
迁移 Codemod
# Run all React 19 codemods
npx codemod@latest react/19/migration-recipe
# Individual codemods
npx codemod@latest react/19/replace-reactdom-render
npx codemod@latest react/19/replace-string-ref
npx codemod@latest react/19/replace-act-import
npx codemod@latest react/19/replace-use-form-state
npx codemod@latest react/prop-types-typescript
# TypeScript types
npx types-react-codemod@latest preset-19 ./src
导入(最佳实践)
// Named imports (recommended)
import { useState, useEffect, useRef, use } from 'react';
import { createRoot } from 'react-dom/client';
import { useFormStatus } from 'react-dom';
// Default import still works but named preferred
import React from 'react'; // Works but not recommended
错误处理变更
// React 19 error handling options
const root = createRoot(container, {
onUncaughtError: (error, errorInfo) => {
// Errors not caught by Error Boundary
console.error('Uncaught:', error, errorInfo.componentStack);
},
onCaughtError: (error, errorInfo) => {
// Errors caught by Error Boundary
reportToAnalytics(error);
},
onRecoverableError: (error, errorInfo) => {
// Errors React recovered from automatically
console.warn('Recovered:', error);
}
});
Suspense 模式和错误边界请参阅 references/suspense-streaming.md。
React 19.2+ 功能
Activity 组件(19.2)
隐藏/显示 UI,同时保留状态(类似后台标签页):
import { Activity } from 'react';
// State preserved when hidden, Effects cleaned up
<Activity mode={isVisible ? 'visible' : 'hidden'}>
<ExpensiveComponent />
</Activity>
useEffectEvent Hook(19.2)
从 Effects 中提取非响应式逻辑,无需添加依赖项:
import { useEffect, useEffectEvent } from 'react';
function Chat({ roomId, theme }) {
// Reads theme without making it a dependency
const onConnected = useEffectEvent(() => {
showNotification(`Connected!`, theme);
});
useEffect(() => {
const conn = connect(roomId);
conn.on('connected', onConnected);
return () => conn.disconnect();
}, [roomId]); // theme NOT in deps - won't reconnect on theme change
}
完整的 19.1+ 和 19.2 功能请参阅 references/react-19-2-features.md。
参考文档
| 文档 | 内容 |
|---|---|
| paradigm-shifts.md | 心智模型变化——如何在 React 19 中进行思考 |
| anti-patterns.md | 应停止的做法——过时模式 |
| react-19-2-features.md | React 19.1+ 和 19.2 功能(Activity、useEffectEvent) |
| new-hooks.md | 19.0 Hooks 的完整 API |
| server-components.md | RSC、Server Actions、指令 |
| suspense-streaming.md | Suspense、流式传输、错误处理 |
| react-compiler.md | 自动记忆化详解 |
| deprecations.md | 已移除的 API 及迁移指南 |
| typescript-changes.md | 类型变更和 codemod |