
一句话:React 18 的主线是「并发渲染」(
createRoot、自动批处理、useTransition/useDeferredValue/useId/useSyncExternalStore、Suspense SSR);React 19 的主线是「把异步与表单做成一等公民」(Actions、use、ref作为 prop、文档元数据、RSC 稳定、React Compiler)。
一、结论先行
问题:React 18 和 19 分别带来了什么变化?
一句话回答:
React 18 = 「并发渲染 + 自动批处理 + SSR 流式 + 一批新 Hook」
React 19 = 「Actions(表单/异步统一)+ use + ref 作为 prop +
文档元数据 + RSC 稳定 + React Compiler」
⭐ 记忆口诀:
18:并发(Concurrent)、自动批处理、流式 SSR、useTransition/useDeferredValue/useId/useSyncExternalStore
19:Actions、use、ref 当 prop、元数据提升、RSC、Compiler
二、React 18 的核心变化
| 变化 | 内容 | 关键点 |
|---|---|---|
createRoot |
新渲染入口 | 启用并发能力的前提(ReactDOM.render 是 legacy) |
| 自动批处理 | 所有场景都批处理 | setTimeout/Promise/原生事件也合并(跨 await 仍分批) |
useTransition |
标记「非紧急更新」 | 返回 isPending |
useDeferredValue |
返回值的「滞后版本」 | 适合「值来自 props」的场景 |
useId |
SSR 一致的唯一 ID | 无障碍属性关联(不能当 key) |
useSyncExternalStore |
订阅外部数据源的官方方式 | 解决并发下的「撕裂」 |
useInsertionEffect |
给 CSS-in-JS 库用 | 在「布局 effect 之前」插样式 |
| Suspense 的 SSR 支持 | renderToPipeableStream + 流式渲染 |
shell 先发、慢的部分后到 |
| StrictMode 更严格 | Effect「挂载 → 卸载 → 再挂载」 | 暴露「清理缺失」 |
// ① createRoot(必须改入口,否则新特性不生效)
import { createRoot } from 'react-dom/client';
createRoot(document.getElementById('root')!).render(<App />);
// ② 自动批处理(所有场景)
setTimeout(() => {
setA(1);
setB(2); // ⭐ React 18:合并成一次渲染(React 17:两次)
}, 0);
// ③ 并发特性(把更新分成紧急/非紧急)
const [isPending, startTransition] = useTransition();
setKeyword(value); // 紧急:输入框回显
startTransition(() => setResults(filter(value))); // 非紧急:可被打断
// ④ useId(SSR 安全)
const id = useId();
<label htmlFor={id}>用户名</label>
<input id={id} />
// ⑤ useSyncExternalStore(订阅外部数据,防撕裂)
const isOnline = useSyncExternalStore(subscribe, () => navigator.onLine, () => true);
// ⑥ 流式 SSR(服务端)
renderToPipeableStream(<App />, { onShellReady: () => pipe(res) });
⭐ React 18 的三个「必须知道」:
① 必须改 `createRoot`,否则自动化批处理与并发能力都不生效
② 「自动批处理」可能改变「依赖渲染次数」的代码行为(升级风险点)
③ StrictMode 会「挂载 → 卸载 → 再挂载」,暴露「清理缺失」
三、React 19 的核心变化
| 变化 | 内容 | 关键点 |
|---|---|---|
| Actions | <form action={fn}> + useActionState + useFormStatus |
统一「提交 + loading + 错误」 |
useOptimistic |
乐观更新 | 回滚是自动的(action 结束即丢弃) |
use |
读 Promise / Context | 可在条件/循环里用(与 useContext 不同) |
ref 作为 prop |
函数组件可直接接收 ref |
不再必须 forwardRef |
| 回调 ref 清理函数 | ref={(el) => { ...; return () => {...} }} |
更自然的资源释放 |
<Context> 作 Provider |
可写 <ThemeContext value={...}> |
少写 .Provider |
| 文档元数据提升 | <title> / <meta> / <link> 自动进 <head> |
不再需要 Head 管理库 |
| Server Components | 正式稳定 | 代码不进客户端包、可直接查库 |
| Server Actions | 'use server' 标记的函数 |
表单可直接调用服务端逻辑 |
| React Compiler | 编译时自动记忆化 | 减少手写 useMemo/useCallback |
| 移除的 API | defaultProps(函数组件)、字符串 ref、propTypes、useFormState |
需要改代码 |
// ① Actions:表单的直接写法
async function createPost(formData: FormData) {
'use server';
await db.post.create({ data: { title: formData.get('title') as string } });
revalidatePath('/posts');
}
function NewPostForm() {
return (
<form action={createPost}>
<input name="title" required />
<SubmitButton />
</form>
);
}
// 提交状态(在 form 的子组件里)
'use client';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? '提交中…' : '创建'}</button>;
}
// ② useActionState:统一管理「返回值 + pending」
const [state, formAction, isPending] = useActionState(
async (prevState, formData) => {
const res = await api.signup(formData);
return res.ok ? { success: true } : { error: '注册失败' };
},
{ error: '' }
);
<form action={formAction}>
<button disabled={isPending}>{isPending ? '提交中…' : '注册'}</button>
{state.error && <p role="alert">{state.error}</p>}
</form>;
// ③ useOptimistic:乐观更新(回滚自动)
const [optimistic, addOptimistic] = useOptimistic(comments, (current, text: string) => [
...current,
{ id: `temp-${Date.now()}`, text, pending: true },
]);
async function formAction(formData: FormData) {
addOptimistic(formData.get('text') as string); // ⭐ 立刻更新 UI
await addComment(formData.get('text') as string); // 失败会自动回滚
}
// ④ use:读 Promise 与 Context(可以在条件/循环里用)
import { use, Suspense } from 'react';
function Comments({ promise }: { promise: Promise<Comment[]> }) {
const comments = use(promise); // ⭐ 未就绪时挂起(配合 Suspense)
return <ul>{comments.map((c) => <li key={c.id}>{c.text}</li>)}</ul>;
}
// Context 也可以在条件里读(useContext 不行)
function Comp({ show }: { show: boolean }) {
if (show) {
const theme = use(ThemeContext); // ✅ 合法
}
}
// ⚠️ 但 Promise 必须【稳定】(不能在渲染中创建,否则无限挂起)
// ⑤ ref 作为普通 prop(不再必须 forwardRef)
interface Props {
label: string;
ref?: React.Ref<HTMLInputElement>;
}
function MyInput({ label, ref }: Props) {
return (
<label>
{label}
<input ref={ref} />
</label>
);
}
// ⚠️ 需要用 @types/react@19 才能正确推导
// ⑥ 回调 ref 可以返回清理函数
<input
ref={(el) => {
el?.focus();
return () => console.log('清理'); // 卸载或 ref 变化时调用
}}
/>
// ⑦ 文档元数据自动提升(不需要 react-helmet)
function BlogPost({ post }: { post: Post }) {
return (
<article>
<title>{post.title}</title>
<meta name="description" content={post.excerpt} />
<h1>{post.title}</h1>
</article>
);
}
四、React 18 与 19 的对比总结
| 维度 | React 18 | React 19 |
|---|---|---|
| 主线 | 并发渲染 | 异步与表单(Actions) |
| 渲染入口 | createRoot(新增) |
沿用 createRoot |
| 批处理 | 自动批处理(所有场景) | 不变 |
| 表单 | 自己管 state | Actions(useActionState/useFormStatus) |
| 乐观更新 | 手动(onMutate + 回滚) |
useOptimistic(自动回滚) |
| 读 Promise | ❌ | ✅ use |
ref |
必须 forwardRef |
可直接作为 prop |
| 元数据 | 需要第三方库 | 内置提升 |
| 服务端组件 | 实验 | 稳定 |
| 记忆化 | 手动 + memo |
React Compiler(自动) |
| 移除的 API | ReactDOM.render 等 |
defaultProps(函数组件)、字符串 ref、propTypes、useFormState |
⭐ 一句话串联:
React 18 解决「**渲染能不能被打断**」(并发),
React 19 解决「**异步与表单怎么写更简单**」(Actions),
并顺手把「人体工学」(ref/元数据)与「架构」(RSC/Compiler)补齐。
五、升级注意点(18 与 19 都要看)
⭐ React 18 升级:
① 必须改入口为 `createRoot`(否则新特性都不生效)
② 检查「依赖渲染次数」的代码(自动批处理会改变行为)
③ 补齐 Effect 的清理函数(StrictMode 会「挂载→卸载→再挂载」)
④ 升级第三方库(CSS-in-JS、状态管理库可能需要新版本)
⑤ 更新类型包(@types/react 与 @types/react-dom 版本要匹配)
⑥ SSR 改为 `renderToPipeableStream`(或先用兼容 API)
⑦ 测试里若出现「请求发两次」,通常是「没处理取消」的问题
⭐ React 19 升级:
① 移除的历史 API:`defaultProps`(函数组件)、字符串 ref、`propTypes`、
`useFormState`(改 `useActionState`)
② 类型包升级到 `@types/react@19`(尤其 `ref` 作为 prop 的类型推导)
③ 用 RSC 的框架要注意 `"use client"` 的边界划分
④ 第三方库兼容(表单库、UI 库可能需要新版本)
⑤ React Compiler 是可选特性:先用 ESLint 插件扫描「不纯的组件」,
再小范围试用并度量(注意「有副作用的组件会被跳过」)
六、常见追问
Q: 用了 React 18 但没改 createRoot,会怎样?
A: 会跑在「legacy 模式」——自动批处理与并发特性都【不生效】。
控制台会有警告(ReactDOM.render is no longer supported)。
Q: React 18 的自动批处理会让「我的代码」出问题吗?
A: 如果你的代码「依赖『每次 setState 都触发一次渲染』」(少见但不罕见),
行为会变(合并成一次)。通常说明那段代码本身不该依赖渲染次数。
表现可能是「动画/日志少了一次」。
Q: React 19 的 `use` 和 `useContext` 有什么区别?
A: ① `use(Context)` 可以【在条件/循环里调用】(`useContext` 必须遵循 Hook 规则);
② `use` 还能【读 Promise】(配合 Suspense)。
注意:`use` 仍必须在【渲染期间】调用(不能放事件处理/Effect 里)。
Q: Actions 解决了什么问题?
A: 把过去的三块重复代码统一了:
① 手动 preventDefault + 收集 FormData → 现在 `<form action={fn}>` 自动处理
② 手动管理 loading → 现在 `useActionState` 的 `isPending` 或 `useFormStatus`
③ 手动管理结果/错误/乐观更新 → 现在 `useActionState` + `useOptimistic`
Q: Server Actions 安全吗?
A: 它是【公开的 HTTP 端点】——任何人都能直接调用。
所以必须:① 在函数内做【鉴权】(从 cookie/session 取用户,
绝不信任客户端传的 userId);② 校验所有输入;③ 不返回敏感数据。
Q: React Compiler 会取代 useMemo 吗?
A: 会【大幅减少手写】,但不会完全取代——
它只覆盖「局部记忆化」,结构性问题(Context、children 透传、虚拟滚动)
它管不了;而且它【跳过不纯的组件】。
Q: 18 和 19 哪个「变化更大」?
A: 「性质不同」——18 是「渲染架构的升级」(并发,影响面极广但需要显式使用),
19 是「开发体验与能力的补齐」(Actions/use/ref,用起来更顺手)。
18 的「隐性影响」更大(批处理、StrictMode 会改变既有行为),
19 的「API 删除」需要改代码但对新项目更友好。
Q: 现在应该用 React 19 吗?
A: 新项目可以用(生态已跟上),但要注意:
① 依赖的库是否支持 19;② 团队对 RSC/Actions 的熟悉度;
③ Compiler 作为「可选优化」先小范围试点。
存量项目可以「先升级到 18(改 createRoot + 修 StrictMode 问题)」,
再评估 19。
七、常见坑
// ⚠️ React 18 相关
// ① 用 ReactDOM.render(自动批处理与并发特性都不生效)
// ② 依赖「每次 setState 都渲染一次」
// ③ StrictMode 下「没写清理」→ 请求两次、订阅两次
// ④ 忘记「升级 @types/react」→ 类型报错
// ⑤ 用「总渲染次数」判断逻辑正确性
// ⑥ 以为「createRoot 之后所有更新都并发」(默认仍是 Sync/Default 优先级)
// ⚠️ React 19 相关
// ⑦ 继续用 defaultProps(函数组件上已移除 → 失效)
// ⑧ 用字符串 ref(已移除)
// ⑨ 在 useOptimistic 里「不在 action 中调用 addOptimistic」
// ⑩ 在渲染中创建 Promise 传给 use()(无限挂起)
// ⑪ 把 useId 当 key 或 CSS 选择器
// ⑫ Server Action 里不做鉴权(它是公开端点)
// ⑬ 'use client' 写在顶层布局(整个应用都变成客户端代码)
// ⑭ 给 Client Component 传「不可序列化的 props」(函数、类实例)
// ⑮ 认为「有 Compiler 就不用管性能了」
// ⑯ 忘记「React 19 需要 @types/react@19」(ref 作为 prop 的类型推导)
// ⑰ 期待「React 19 仍然支持 useFormState」(改名为 useActionState)
// ⑱ 在「RSC 环境」里用「浏览器 API 或客户端 Hook」(服务端组件不允许)
// ⑬ 的正确做法(把边界下沉)
// page.tsx(Server Component)
import { LikeButton } from './like-button'; // 只有这个进客户端包
import { HeavyChart } from './heavy-chart'; // ⭐ Server Component(不进客户端包)
export default async function Page() {
const data = await getData();
return (
<>
<HeavyChart data={data} />
<LikeButton postId={data.id} />
</>
);
}
八、面试延伸
- 「React 18 带来了什么?」
核心是「并发渲染(Concurrent Rendering)」——渲染过程可以「中断、丢弃、按优先级调度」,从而让长渲染不阻塞用户输入。围绕它的 API 有:createRoot(启用前提)、useTransition/useDeferredValue/startTransition(标记非紧急更新)、useId(SSR 一致的 ID)、useSyncExternalStore(订阅外部数据,避免撕裂)、useInsertionEffect(CSS-in-JS 用)、Suspense 的流式 SSR(renderToPipeableStream)。此外还有自动批处理(所有场景都合并更新)与 StrictMode 更严格(Effect「挂载 → 卸载 → 再挂载」)。 -
「React 19 带来了什么?」
三条主线:① 异步与表单(Actions)——<form action>、useActionState(返回值 +pending)、useFormStatus(子组件读提交状态)、useOptimistic(自动回滚的乐观更新),配合 Server Actions 可直接调用服务端逻辑;② 人体工学改进——ref作为普通 prop(不再必须forwardRef)、回调 ref 支持返回清理函数、<Context>可直接作 Provider、<title>/<meta>自动提升到<head>;③ 架构能力——Server Components 正式稳定、React Compiler 提供编译时自动记忆化。同时移除了defaultProps(函数组件)、字符串ref、propTypes、useFormState等历史 API。 -
「React 18 的自动批处理是什么?有什么影响?」
指「所有场景下的多次状态更新都会被合并成一次渲染」——包括setTimeout、Promise.then、原生事件监听(React 17 及之前这些场景不会批处理)。前提是使用createRoot。影响:① 减少渲染次数、避免中间态(好处);② 如果代码「依赖『每次setState都触发一次渲染』」,行为会变(升级风险点);③ 被await或多个宏任务分隔的更新依然分成多个批次。 -
「
use和useContext/useEffect有什么区别?」
① 与useContext的区别——use(Context)可以在条件与循环里调用(useContext必须遵守 Hook 的顶层调用规则);② 与useEffect的区别——useEffect是「副作用」(渲染之后执行),而use是「在渲染期间读取一个值」(读 Context 或「读 Promise 并挂起」)。注意:use仍必须在渲染期间调用(不能放事件处理或 Effect 里),且「Promise 必须稳定」(不能在渲染中创建)。 -
「Actions 是什么?解决了什么问题?」
Actions 是 React 19 对「表单提交 / 数据变更」的统一抽象。它解决了过去的三块重复代码:① 手动preventDefault+ 收集FormData——现在写<form action={fn}>,React 自动处理提交与重置;② 手动管理loading——现在用useActionState返回的isPending,或子组件里的useFormStatus;③ 手动管理结果/错误/乐观更新——现在useActionState统一管返回值,useOptimistic管乐观更新(失败自动回滚)。配合 Server Actions 还能直接在表单里调用服务端函数,少写一层 API。 -
「Server Actions 有什么安全风险?」
它是「公开的 HTTP 端点」——任何人都可以直接构造请求调用它(不经过你的 UI)。所以必须:① 鉴权——在函数内部检查「当前用户是否有权执行」(从 cookie/session 读取,绝不能相信客户端传来的用户 ID);② 输入校验——所有参数都要校验(不能假设调用者就是你的表单);③ 不返回敏感数据。常见错误是「以为『只在表单里用』就安全」。 -
「Server Components 和 Client Components 的区别?」
Server Component(默认)在服务端执行:可以是async、能直接访问数据库与文件系统、代码不会进入客户端包;但不能有状态(useState)、事件处理(onClick)、副作用(useEffect)、浏览器 API(window)。Client Component 用"use client"标记,可以有交互与状态。关键规则:"use client"标记的是一个「模块图入口」——该文件及其所有导入的模块都会变成客户端代码,所以应当「把边界下沉到叶子交互组件」。 -
「React Compiler 会取代手写优化吗?」
大幅减少但不会完全取代。Compiler 在构建时分析组件并自动插入记忆化(相当于自动写useMemo/useCallback/memo),它依赖「组件是纯函数」的假设——不纯的组件会被跳过。它只覆盖「局部记忆化」,「结构性问题」(Context 的订阅粒度、children透传、状态下移、虚拟滚动、代码分割)它管不了。所以「理解渲染机制与引用稳定」依然必要,只是「手写的频率下降」。
一句话速记
React 18 = 并发(
createRoot(必须改入口)、自动批处理(所有场景)、useTransition/useDeferredValue/startTransition、useId、useSyncExternalStore、useInsertionEffect、Suspense 流式 SSR(renderToPipeableStream)、StrictMode 「挂载→卸载→再挂载」);React 19 = 异步与表单(Actions:<form action>+useActionState+useFormStatus+useOptimistic(自动回滚)、use(可在条件/循环里读 Context、可读 Promise)、ref作为普通 prop(不再必须forwardRef) + 回调 ref 清理函数、<Context>直接作 Provider、元数据自动提升、Server Components 稳定、React Compiler),并移除defaultProps/字符串 ref/propTypes/useFormState;升级注意「18 要改createRoot+ 检查依赖渲染次数的代码 + 补清理」「19 要改废弃 API + 升@types/react@19+ 下沉'use client'边界」;Compiler 减少手写但不减少理解。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!