陌上人如玉
公子世无双

4.11 React 19 新特性

4.11 React 19 新特性

一句话:React 19 的主线是「把异步与表单做成一等公民」——Actions(useActionState / useFormStatus / useOptimistic)、use API、ref 作为普通 prop、文档元数据提升;同时引入 React Compiler(自动记忆化)与 Server Components 的正式落地。

一、总览

特性 内容 价值
Actions <form action={fn}> + useActionState + useFormStatus + useOptimistic 统一「提交 + loading + 错误 + 乐观更新」
use 在渲染中读取 Promise 与 Context 配合 Suspense 做数据获取
ref 作为 prop 函数组件可直接接收 ref(不再必须 forwardRef) 少一层包装
ref 清理函数 回调 ref 可返回清理函数 更自然的资源释放
<Context> 作 Provider 可直接 <ThemeContext value={...}> 少写 .Provider
文档元数据提升 <title> / <meta> / <link> 自动提升到 <head> 不用手写 Head 管理
样式表与脚本 <link rel="stylesheet" precedence>、<script async> 的去重与顺序管理 更好的加载控制
Server Components 正式支持(稳定) 减少客户端包体积、直接访问数据库
Server Actions "use server" 标记的函数可被表单直接调用 少写一层接口
React Compiler 编译时自动记忆化 减少手写 useMemo/useCallback
移除的 API defaultProps(函数组件)、字符串 ref、propTypes、ReactDOM.render 等 清理历史包袱

二、Actions:表单的现代写法

// ① 最基础:<form action> 直接接收函数(React 会自动处理 preventDefault 与 reset)
function SimpleForm() {
  async function handleSubmit(formData: FormData) {
    'use server';                              // Server Action(或普通 async 函数)
    await db.createTodo({ text: formData.get('text') as string });
  }

  return (
    <form action={handleSubmit}>
      <input name="text" required />
      <button type="submit">添加</button>
    </form>
  );
}
// ② useActionState:统一管理「提交结果 + loading」
import { useActionState } from 'react';

function SignupForm() {
  const [state, formAction, isPending] = useActionState(
    async (prevState: State, formData: FormData) => {
      const res = await api.signup(formData);
      if (!res.ok) return { error: '注册失败,请重试' };
      return { success: true, error: '' };
    },
    { error: '', success: false }                 // 初始 state
  );

  return (
    <form action={formAction}>
      <input name="email" type="email" required />
      <input name="password" type="password" required />
      <button disabled={isPending}>{isPending ? '提交中…' : '注册'}</button>
      {state.error && <p role="alert">{state.error}</p>}
    </form>
  );
}
// ③ useFormStatus:在「子组件」里读取父级 form 的提交状态
import { useFormStatus } from 'react';

function SubmitButton() {
  const { pending, data, method, action } = useFormStatus();
  return <button disabled={pending}>{pending ? '提交中…' : '提交'}</button>;
}
// ⚠️ 必须在 <form> 的子组件里调用(它读的是「最近的 form」)

function Form() {
  return (
    <form action={myAction}>
      <input name="x" />
      <SubmitButton />                        {/* ⭐ 这里能读到 pending */}
    </form>
  );
}
// ④ useOptimistic:乐观更新
import { useOptimistic } from 'react';

function Comments({ comments, addComment }: { comments: Comment[]; addComment: (text: string) => Promise<void> }) {
  const [optimisticComments, addOptimisticComment] = useOptimistic(
    comments,
    (current, newText: string) => [...current, { id: 'temp', text: newText, pending: true }]
  );

  async function formAction(formData: FormData) {
    const text = formData.get('text') as string;
    addOptimisticComment(text);                 // ⭐ 立即显示
    await addComment(text);                     // 后台提交(失败时 React 会自动回滚)
  }

  return (
    <>
      <ul>
        {optimisticComments.map((c) => (
          <li key={c.id} style={{ opacity: 'pending' in c && c.pending ? 0.5 : 1 }}>{c.text}</li>
        ))}
      </ul>
      <form action={formAction}>
        <input name="text" />
        <button>发送</button>
      </form>
    </>
  );
}
API 作用 注意
<form action={fn}> 提交时调用 fn(formData) 自动处理 preventDefault 与表单重置
useActionState 管理 action 的返回值 + pending 返回 [state, action, isPending]
useFormStatus 读取所属 form 的提交状态 必须在 form 的子组件里用
useOptimistic 乐观更新(自动回滚) 需在 action 内部调用 addOptimistic
Server Actions "use server" 标记的函数 可直接在表单里调用服务端逻辑

三、use:在渲染中读取 Promise 与 Context

// ① 读 Promise(配合 Suspense)
import { use, Suspense } from 'react';

function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
  const comments = use(commentsPromise);        // ⭐ 挂起直到 resolve
  return <ul>{comments.map((c) => <li key={c.id}>{c.text}</li>)}</ul>;
}

function Page() {
  const commentsPromise = fetchComments();      // ⚠️ 不要在组件里创建(会每次新建)
  return (
    <Suspense fallback={<Skeleton />}>
      <Comments commentsPromise={commentsPromise} />
    </Suspense>
  );
}
// ② 读 Context(可以在条件语句里用!与 useContext 不同)
import { use } from 'react';

function Component({ showTheme }: { showTheme: boolean }) {
  if (showTheme) {
    const theme = use(ThemeContext);            // ✅ 允许(use 不是 Hook 规则限制的对象)
    return <div>{theme}</div>;
  }
  return <div>无主题</div>;
}
// ⚠️ 虽然允许,但仍要谨慎(会让组件对 Context 的依赖变得隐式)

// ③ 也可以在循环里用(这是与 useContext 的重要差异)
{items.map((item) => {
  const theme = use(ThemeContext);              // ✅ 合法
  return <Row key={item.id} theme={theme} />;
})}
`use` 与其它 Hook 的差异:

| 维度 | `useContext` | `use(Context)` |
| 可否在条件/循环里 | ❌ | ✅ |
| 可否在普通函数里 | ❌ | ✅(但仍要在渲染期间调用) |
| 读 Promise | ❌ | ✅ |

⚠️ 但 `use` 仍然不能「在事件处理/Effect 里读 Promise」——
   Promise 的读取必须在渲染期间发生(否则会失去 Suspense 的意义)。

四、ref 作为普通 prop(不再必须 forwardRef)

// ✅ React 19:函数组件直接把 ref 当 props 接收
interface Props {
  label: string;
  ref?: React.Ref<HTMLInputElement>;
}

function MyInput({ label, ref }: Props) {
  return (
    <label>
      {label}
      <input ref={ref} />
    </label>
  );
}

// 使用(与之前一样)
const ref = useRef<HTMLInputElement>(null);
<MyInput label="用户名" ref={ref} />;
// ✅ 回调 ref 现在可以返回「清理函数」
<input
  ref={(el) => {
    el?.focus();
    return () => {
      console.log('清理');          // 卸载或 ref 变化时调用
    };
  }}
/>

// 之前(React 18)需要用「额外的一次调用(传 null)」来模拟清理:
<input
  ref={(el) => {
    if (el) el.focus();
    else console.log('清理');       // ⚠️ 回调会以 null 再次调用
  }}
/>
⚠️ 注意事项:
① forwardRef 仍然可用(向后兼容),但不再必需
② 如果用了 TS,@types/react 需要 19 版本才能正确推导「ref 作为 prop」
③ 「ref 是普通 prop」意味着它【可以被展开传递】:
   <MyInput {...props} /> 里如果 props 有 ref,会正常传递

五、Context 作为 Provider

// React 18:必须用 .Provider
<ThemeContext.Provider value={theme}>
  <App />
</ThemeContext.Provider>

// ✅ React 19:可以直接用 Context 本身
<ThemeContext value={theme}>
  <App />
</ThemeContext>

// 两种写法都兼容

六、文档元数据提升

// ✅ React 19:<title> / <meta> / <link> 会自动提升到 <head>(不需要额外库)
function BlogPost({ post }: { post: Post }) {
  return (
    <article>
      <title>{post.title}</title>
      <meta name="description" content={post.excerpt} />
      <meta property="og:title" content={post.title} />
      <link rel="canonical" href={`https://example.com/posts/${post.slug}`} />

      <h1>{post.title}</h1>
      {post.body}
    </article>
  );
}
// ✅ 样式表与脚本也有更好的处理
<link rel="stylesheet" href="/style.css" precedence="default" />
//  ↑ React 会去重、并按 precedence 排序

<script async src="/analytics.js" />
//  ↑ 去重处理(同一个 src 只加载一次)
收益:
① 不再需要 react-helmet / next/head 这类库来做「元数据管理」
② 支持 SSR 流式渲染(元数据可以在流中被提升)
③ 重复的 <link> / <script> 会被自动去重

七、Server Components 与 Server Actions

// ① Server Component(默认):在服务端执行(可以 async)
// app/posts/page.tsx
import { db } from '@/lib/db';

export default async function PostsPage() {
  const posts = await db.post.findMany();        // ⭐ 直接访问数据库(代码不进客户端包)

  return (
    <ul>
      {posts.map((p) => <li key={p.id}>{p.title}</li>)}
    </ul>
  );
}
// ② Client Component:用 "use client" 标记
'use client';
import { useState } from 'react';

export function LikeButton({ postId }: { postId: string }) {
  const [liked, setLiked] = useState(false);     // ⭐ 有状态/事件,必须在客户端
  return <button onClick={() => setLiked((v) => !v)}>{liked ? '已赞' : '点赞'}</button>;
}
// ③ Server Actions:'use server' 标记的函数可以被表单/按钮直接调用
'use server';
export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  await db.post.create({ data: { title } });
  revalidatePath('/posts');
}

// 客户端组件里使用
import { createPost } from './actions';

function NewPostForm() {
  return (
    <form action={createPost}>
      <input name="title" />
      <button>创建</button>
    </form>
  );
}
Server Components 的边界规则(重点):
① Server Component 不能有状态、事件处理器、浏览器 API
② Client Component 用 "use client" 标记,并且【它导入的所有模块都会变成客户端代码】
   → 所以要尽量把边界【下沉到叶子组件】
③ Server Component 可以渲染 Client Component(传递「可序列化的 props」)
④ Client Component 不能直接导入 Server Component(但可以通过 children 传入)

八、React Compiler(自动记忆化)

// 之前:需要手动 useMemo / useCallback / memo
function ExpensiveList({ items, onSelect }: Props) {
  const sorted = useMemo(() => [...items].sort(), [items]);
  const handleSelect = useCallback((id: string) => onSelect(id), [onSelect]);
  return <List items={sorted} onSelect={handleSelect} />;
}

// ✅ 有 Compiler 后:直接写「朴素」代码,编译器自动插入记忆化
function ExpensiveList({ items, onSelect }: Props) {
  const sorted = [...items].sort();              // 编译器会自动 memo 化
  const handleSelect = (id: string) => onSelect(id);
  return <List items={sorted} onSelect={handleSelect} />;
}
React Compiler 的定位与限制:

① 它是【编译时】工具(Babel/SWC 插件),在构建时分析组件并插入记忆化
② 它依赖「组件是纯函数」的假设 → 有副作用的组件会被跳过(并给出提示)
③ 它【不替代】「理解引用稳定」这些概念——因为:
   · 跨组件的引用传递(父组件把函数传给子组件)依然需要注意
   · 有编译器覆盖不到的情况(如手写的外部 store 订阅)
④ 目前是可选特性(需要配置构建工具),生产使用需评估

一句话:它让「默认性能」更好,但不改变「要理解渲染机制」这件事。

九、移除 / 废弃的 API

移除/废弃 替代
defaultProps(函数组件) 默认参数 ({ x = 1 })
字符串 ref(ref="input") 回调 ref / useRef
propTypes TypeScript
ReactDOM.render / hydrate createRoot / hydrateRoot
forwardRef ref 作为 prop(但仍兼容)
useFormState useActionState
ReactDOM.findDOMNode ref
test-utils 的 act React.act

十、常见坑

// ① 把 ref 当普通 prop 但又写了 forwardRef(重复且无必要)
const Comp = forwardRef(function Comp({ ref }: Props) { ... });   // ⚠️ 混用

// ② 在客户端组件里只用「服务端才有的能力」(如直接访问数据库)
'use client';
async function Page() {
  const data = await db.query();       // ❌ 客户端没有 db
}

// ③ "use client" 写在「很高的位置」→ 整个子树都变成客户端代码
'use client';                          // ⚠️ 放在顶层布局文件 → 失去 RSC 收益

// ④ 用 useActionState 但在 action 里「不抛错也不返回」→ 状态不更新
// ⑤ 在 useFormStatus 的「同一个组件」里调用(必须在 form 的子组件里)
function Form() {
  const { pending } = useFormStatus();   // ❌ 读到的是「外层的 form」
  return <form action={fn}>...</form>;
}

// ⑥ 用 use(Promise) 但 Promise 在渲染中创建(每次渲染都是新 Promise)
const p = fetchData();                   // ❌ 每次渲染都发起请求
const data = use(p);                     // 应该由「上层/缓存」提供稳定 Promise

// ⑦ 认为「有 Compiler 就不用管性能了」
// ⑧ 依赖 defaultProps(React 19 已移除,函数组件上会失效)
// ⑨ 元数据提升与「客户端渲染的 title 管理库」重复(会出现两套)
// ⑩ 忘记「Server Component 的 props 必须可序列化」(不能传函数/类实例)
// ⑪ 用 ref 清理函数但返回了「非函数」的值(会被当成清理函数调用)
// ⑫ 升级时忘记同步 @types/react 到 19

十一、面试延伸

  1. 「React 19 有哪些主要变化?」
    三条主线:① 异步与表单——Actions 体系(<form action>、useActionState、useFormStatus、useOptimistic)把「提交 + loading + 错误 + 乐观更新」统一起来,配合 Server Actions 可以直接调用服务端逻辑;② 人体工学改进——ref 作为普通 prop(不再必须 forwardRef)、<Context> 可直接作 Provider、<title>/<meta> 自动提升到 <head>、回调 ref 支持返回清理函数;③ 架构能力——Server Components 正式稳定、React Compiler 提供编译时自动记忆化,同时移除了 defaultProps、字符串 ref、propTypes 等历史 API。

  2. 「什么是 Actions?它解决了什么问题?」
    Actions 是 React 19 对「表单提交 / 数据变更」的统一抽象。它解决了过去的三块重复代码:① 手动 preventDefault + 用 FormData 收集数据(现在写成 <form action={fn}>,React 自动处理);② 手动管理 loading(现在用 useActionState 的 isPending 或子组件里的 useFormStatus);③ 手动管理结果与错误、以及乐观更新(现在分别由 useActionState 的 state 和 useOptimistic 处理,失败自动回滚)。

  3. 「use 和 useContext 有什么区别?」
    两个差异:① use(Context) 可以在条件语句与循环里调用(而 useContext 必须遵守 Hook 的顶层调用规则);② use 还能读取 Promise(配合 Suspense 实现数据获取)。注意:use 仍然只能在渲染期间调用(不能放在事件处理或 Effect 里),并且「在渲染中创建 Promise」是错误用法(每次渲染都会新建)。

  4. 「React 19 里的 ref 有什么变化?」
    两个:① 函数组件可以直接把 ref 当普通 prop 接收(function Comp({ ref }) {}),不再必须用 forwardRef(forwardRef 仍兼容);② 回调 ref 可以返回清理函数(ref={(el) => { ...; return () => {...}; }}),让「订阅与释放」写在一起,比过去「用 null 再次调用回调」的方式更自然。

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

  6. 「什么是 React Compiler?它会替代 useMemo 吗?」
    React Compiler 是一个编译时工具(Babel/SWC 插件),会在构建阶段分析组件并自动插入记忆化(相当于自动帮你写 useMemo/useCallback/memo),因此日常代码可以写得更「朴素」。但它不能完全替代手动优化:① 它依赖「组件是纯函数」的假设(有副作用的组件会被跳过);② 跨组件的引用传递仍需注意;③ 有它覆盖不到的场景(如手写的外部 store 订阅)。结论是「默认性能更好,但仍需理解渲染机制」。

  7. 「Server Components 有什么好处?」
    四点:① 减少客户端包体积——服务端组件的代码不会打包到客户端(如「只用一次的大依赖」「数据库 SDK」);② 更快的数据获取——服务端可以直接访问数据库,省掉一层 HTTP 往返,也避免「客户端瀑布式请求」;③ 更好的安全性——敏感逻辑与凭证留在服务端;④ 与静态/流式渲染配合更好——可先发送静态 shell,慢的部分用 Suspense 流式补充。

  8. 「React 19 升级要注意什么?」
    五点:① 移除的历史 API——defaultProps(函数组件)、字符串 ref、propTypes、useFormState(改 useActionState),需要改代码;② 类型包升级到 @types/react@19(尤其 ref 作为 prop 的类型推导);③ 客户端渲染项目基本无感(Actions/use 都能用),但如果用了 RSC 框架要注意 "use client" 的边界划分;④ 第三方库兼容(表单库、UI 库可能需要新版本);⑤ Compiler 是可选特性——启用前先在切片上验证,注意「有副作用的组件会被跳过」并出现 lint 提示。

一句话速记

React 19 三大主线:异步与表单(Actions:<form action>、useActionState、useFormStatus、useOptimistic,配合 Server Actions)、人体工学改进(ref 作为普通 prop(forwardRef 不再必需)、回调 ref 可返回清理函数、<Context> 直接作 Provider、<title>/<meta> 自动提升到 <head>)、架构能力(Server Components 稳定、React Compiler 自动记忆化);新增 use(可读 Promise 与 Context,可在条件/循环里用,但仍须在渲染期间);移除了 defaultProps / 字符串 ref / propTypes / useFormState;RSC 的边界规则是「"use client" 会把它导入的所有模块都变成客户端代码」→ 边界要下沉。

赞(0) 打赏
未经允许不得转载:陌上寒 » 4.11 React 19 新特性

评论 抢沙发

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

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

微信扫一扫

支付宝扫一扫