陌上人如玉
公子世无双

12.12 React 18 与 19 的变化

12.12 React 18 与 19 的变化

一句话: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} />
    </>
  );
}

八、面试延伸

  1. 「React 18 带来了什么?」
    核心是「并发渲染(Concurrent Rendering)」——渲染过程可以「中断、丢弃、按优先级调度」,从而让长渲染不阻塞用户输入。围绕它的 API 有:createRoot(启用前提)、useTransition / useDeferredValue / startTransition(标记非紧急更新)、useId(SSR 一致的 ID)、useSyncExternalStore(订阅外部数据,避免撕裂)、useInsertionEffect(CSS-in-JS 用)、Suspense 的流式 SSR(renderToPipeableStream)。此外还有自动批处理(所有场景都合并更新)与 StrictMode 更严格(Effect「挂载 → 卸载 → 再挂载」)。

  2. 「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。

  3. 「React 18 的自动批处理是什么?有什么影响?」
    指「所有场景下的多次状态更新都会被合并成一次渲染」——包括 setTimeout、Promise.then、原生事件监听(React 17 及之前这些场景不会批处理)。前提是使用 createRoot。影响:① 减少渲染次数、避免中间态(好处);② 如果代码「依赖『每次 setState 都触发一次渲染』」,行为会变(升级风险点);③ 被 await 或多个宏任务分隔的更新依然分成多个批次。

  4. 「use 和 useContext / useEffect 有什么区别?」
    ① 与 useContext 的区别——use(Context) 可以在条件与循环里调用(useContext 必须遵守 Hook 的顶层调用规则);② 与 useEffect 的区别——useEffect 是「副作用」(渲染之后执行),而 use 是「在渲染期间读取一个值」(读 Context 或「读 Promise 并挂起」)。注意:use 仍必须在渲染期间调用(不能放事件处理或 Effect 里),且「Promise 必须稳定」(不能在渲染中创建)。

  5. 「Actions 是什么?解决了什么问题?」
    Actions 是 React 19 对「表单提交 / 数据变更」的统一抽象。它解决了过去的三块重复代码:① 手动 preventDefault + 收集 FormData——现在写 <form action={fn}>,React 自动处理提交与重置;② 手动管理 loading——现在用 useActionState 返回的 isPending,或子组件里的 useFormStatus;③ 手动管理结果/错误/乐观更新——现在 useActionState 统一管返回值,useOptimistic 管乐观更新(失败自动回滚)。配合 Server Actions 还能直接在表单里调用服务端函数,少写一层 API。

  6. 「Server Actions 有什么安全风险?」
    它是「公开的 HTTP 端点」——任何人都可以直接构造请求调用它(不经过你的 UI)。所以必须:① 鉴权——在函数内部检查「当前用户是否有权执行」(从 cookie/session 读取,绝不能相信客户端传来的用户 ID);② 输入校验——所有参数都要校验(不能假设调用者就是你的表单);③ 不返回敏感数据。常见错误是「以为『只在表单里用』就安全」。

  7. 「Server Components 和 Client Components 的区别?」
    Server Component(默认)在服务端执行:可以是 async、能直接访问数据库与文件系统、代码不会进入客户端包;但不能有状态(useState)、事件处理(onClick)、副作用(useEffect)、浏览器 API(window)。Client Component 用 "use client" 标记,可以有交互与状态。关键规则:"use client" 标记的是一个「模块图入口」——该文件及其所有导入的模块都会变成客户端代码,所以应当「把边界下沉到叶子交互组件」。

  8. 「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 减少手写但不减少理解。

赞(0) 打赏
未经允许不得转载:陌上寒 » 12.12 React 18 与 19 的变化

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫