第 28 章

React 19 新特性全解:Actions、use()、编译器与并发改进

第28章:React 19 新特性全解:Actions、use()、编译器与并发改进

React 19 围绕一个核心主题展开:让代码更接近'描述意图'而不是'描述实现'。

本章核心问题:React 19 的 Actions 模型、use() Hook 和 Compiler 分别解决了什么问题? 读完本章你将理解:


Level 1 · 你需要知道的(1-3年经验)

Actions:异步操作的声明式模型

问题:表单提交的状态管理噩梦

在 React 18 时代,处理一个异步表单提交需要手动管理大量状态:

// React 18 中的典型异步表单处理
function UpdateNameForm() {
  const [name, setName] = useState('');
  const [isPending, setIsPending] = useState(false);
  const [error, setError] = useState(null);
  const [optimisticName, setOptimisticName] = useState('');
  
  async function handleSubmit(e) {
    e.preventDefault();
    setIsPending(true);
    setError(null);
    setOptimisticName(name); // 乐观更新
    
    try {
      await updateName(name);
    } catch (err) {
      setError(err.message);
      setOptimisticName(''); // 回滚乐观更新
    } finally {
      setIsPending(false);
    }
  }
  
  return (
    <form onSubmit={handleSubmit}>
      <input value={name} onChange={e => setName(e.target.value)} />
      {isPending && <Spinner />}
      {error && <p>{error}</p>}
    </form>
  );
}

这段代码并不复杂,但每个表单都要重复这个模式,不仅繁琐,而且容易遗漏错误处理。

Actions:用异步函数替代事件处理器

React 19 引入了 Actions 的概念:可以将异步函数直接传给表单的 action 属性,React 会自动管理 pending 状态、错误状态和乐观更新:

// React 19 的 Actions 模型
function UpdateNameForm() {
  const [error, submitAction, isPending] = useActionState(
    async (prevState, formData) => {
      const name = formData.get('name');
      const error = await updateName(name);
      if (error) return error;
      return null;
    },
    null // 初始状态
  );
  
  return (
    <form action={submitAction}>
      <input name="name" />
      {isPending && <Spinner />}
      {error && <p>{error}</p>}
      <button type="submit" disabled={isPending}>Update</button>
    </form>
  );
}

useActionState 接收一个异步函数,返回 [state, action, isPending] 元组。当表单提交时,React 自动将 isPending 设为 true,等待异步函数完成后更新 state,并将 isPending 重置为 false。

useFormStatus:读取父表单状态

useFormStatus 允许子组件读取其所属表单的提交状态,无需通过 props 传递:

// packages/react-dom/src/client/ReactDOMFormElement.js 提供支持
import { useFormStatus } from 'react-dom';

function SubmitButton() {
  // 自动读取最近的父 <form> 的状态
  const { pending, data, method, action } = useFormStatus();
  
  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Submitting...' : 'Submit'}
    </button>
  );
}

function Form() {
  return (
    <form action={someAction}>
      <input name="data" />
      <SubmitButton /> {/* 自动获取表单 pending 状态 */}
    </form>
  );
}

useFormStatus 底层使用 Context 机制,React 在处理 <form action={fn}> 时会创建一个 FormStatus Context,所有子组件可以通过 useFormStatus 订阅它。

useOptimistic:乐观更新的声明式实现

import { useOptimistic } from 'react';

function MessageList({ messages, sendMessage }) {
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (currentMessages, newMessage) => [
      ...currentMessages,
      { text: newMessage, status: 'sending' }
    ]
  );
  
  async function formAction(formData) {
    const message = formData.get('message');
    // 立即显示乐观消息(不等待服务器响应)
    addOptimisticMessage(message);
    // 发送到服务器
    await sendMessage(message);
    // 服务器响应后,optimisticMessages 自动回退到 messages(真实数据)
  }
  
  return (
    <div>
      {optimisticMessages.map((msg, i) => (
        <p key={i} style={{ opacity: msg.status === 'sending' ? 0.7 : 1 }}>
          {msg.text}
        </p>
      ))}
      <form action={formAction}>
        <input name="message" />
        <button type="submit">Send</button>
      </form>
    </div>
  );
}

useOptimistic 的实现原理是:在 action 执行期间,它使用"乐观状态"(通过 addOptimisticMessage 应用的中间状态)替代真实状态进行渲染。当 action 完成时(无论成功还是失败),React 自动切换回真实状态,丢弃乐观状态。这个切换发生在一个 transition 中,所以不会引起页面闪烁。

use():在渲染中读取资源

use() 是 React 19 中最具革命性的 Hook——它打破了以往"只能在 React 组件函数顶层调用 Hook"的惯例,可以在条件语句、循环,乃至普通函数中调用。

读取 Promise

import { use, Suspense } from 'react';

// 在组件外创建 Promise(不能在组件内创建,避免每次渲染重新创建)
const userPromise = fetchUser(userId);

function UserProfile() {
  // use() 读取 Promise — 如果 Promise 还未 resolve,组件会 suspend
  const user = use(userPromise);
  return <div>{user.name}</div>;
}

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <UserProfile />
    </Suspense>
  );
}

use(promise) 的工作机制与 throw promise(Suspense 的传统触发方式)类似,但更加简洁:当 Promise 还未 resolve 时,use() 会抛出 Promise,触发最近的 Suspense 边界显示 fallback;当 Promise resolve 后,React 重新渲染组件,use() 直接返回 resolved 的值。

// use() 可以在条件语句中使用
function DataDisplay({ condition, promise1, promise2 }) {
  const data = use(condition ? promise1 : promise2);
  return <div>{data}</div>;
}

读取 Context

use() 也可以读取 Context,与 useContext 等价,但可以在条件语句中使用:

function Button({ show }) {
  if (!show) return null; // 可以在这之后调用 use()
  
  const theme = use(ThemeContext); // 等价于 useContext(ThemeContext)
  return <button style={{ color: theme.color }}>Click</button>;
}

这个特性让之前许多需要重构组件结构的场景变得简单——你不再需要为了"有条件地使用 Context"而把组件拆成两个。

use() 的内部实现

// packages/react/src/ReactHooks.js(简化)
export function use(usable) {
  return ReactCurrentDispatcher.current.use(usable);
}

// packages/react-reconciler/src/ReactFiberHooks.js
function useThenable(thenable) {
  // 追踪当前 thenable 的状态
  const index = thenableIndexCounter++;
  const thenableState = getThenableStateAfterSuspending();
  
  // 检查这个 Promise 是否已经 resolve
  switch (thenable.status) {
    case 'fulfilled': {
      const fulfilledValue = thenable.value;
      return fulfilledValue;
    }
    case 'rejected': {
      const rejectedReason = thenable.reason;
      throw rejectedReason;
    }
    default: {
      const prevThenableAtIndex = thenableState !== null
        ? thenableState.get(index)
        : null;
      
      if (prevThenableAtIndex === thenable) {
        // 同一个 Promise,检查状态
        switch (thenable.status) {
          case 'fulfilled': return thenable.value;
          case 'rejected': throw thenable.reason;
        }
      }
      
      // 订阅 Promise 状态变化
      switch (thenable.status) {
        case 'pending':
          thenable.then(
            (fulfilledValue) => {
              if (thenable.status === 'pending') {
                thenable.status = 'fulfilled';
                thenable.value = fulfilledValue;
              }
            },
            (error) => {
              if (thenable.status === 'pending') {
                thenable.status = 'rejected';
                thenable.reason = error;
              }
            }
          );
          break;
      }
      
      // 抛出 Promise,触发 Suspense
      throw thenable;
    }
  }
}

React Compiler:自动记忆化

React Compiler(前身是 React Forget 项目)是 React 19 生态中最具野心的工程:一个将 React 组件的手动 useMemo/useCallback 优化自动化的编译器。

工作原理

React Compiler 在构建时(而非运行时)分析组件代码,判断哪些值和函数在什么条件下保持稳定,并自动插入记忆化逻辑。

// 你写的代码
function ProductList({ category, onSelect }) {
  const filteredProducts = products.filter(p => p.category === category);
  
  return filteredProducts.map(product => (
    <ProductCard
      key={product.id}
      product={product}
      onSelect={onSelect}
    />
  ));
}

// React Compiler 编译后的输出(概念性展示)
function ProductList({ category, onSelect }) {
  const $ = useMemoCache(3);
  
  let filteredProducts;
  if ($[0] !== category) {
    filteredProducts = products.filter(p => p.category === category);
    $[0] = category;
    $[1] = filteredProducts;
  } else {
    filteredProducts = $[1];
  }
  
  let result;
  if ($[2] !== filteredProducts || $[3] !== onSelect) {
    result = filteredProducts.map(product => (
      <ProductCard
        key={product.id}
        product={product}
        onSelect={onSelect}
      />
    ));
    $[2] = filteredProducts;
    $[3] = onSelect;
    $[4] = result;
  } else {
    result = $[4];
  }
  
  return result;
}

编译器使用了一个名为 useMemoCache 的内部 Hook(不对外公开)来存储缓存值。它分析组件的数据流图,识别哪些计算依赖于哪些 props/state,然后只在依赖变化时重新计算。

组件纯度假设

React Compiler 的工作前提是组件和 Hook 必须是纯的(相同输入,相同输出,无副作用)。编译器会分析代码并检测潜在的不纯操作。如果检测到不纯,编译器会跳过对该组件的优化,而不是生成错误的代码。

// 编译器会跳过优化:直接修改 props 或外部变量
function BadComponent({ items }) {
  items.push('new item'); // 违反纯度,编译器会放弃优化
  return <ul>{items.map(i => <li key={i}>{i}</li>)}</ul>;
}

Level 2 · 它是怎么运行的(3-5年经验)

ref 作为 prop:告别 forwardRef

React 19 中,函数组件可以直接接收 ref 作为 prop,不再需要 forwardRef 包装:

// React 18:需要 forwardRef
const MyInput = forwardRef(function MyInput({ placeholder }, ref) {
  return <input ref={ref} placeholder={placeholder} />;
});

// React 19:直接接收 ref prop
function MyInput({ placeholder, ref }) {
  return <input ref={ref} placeholder={placeholder} />;
}

// 使用方式相同
function Parent() {
  const ref = useRef(null);
  return <MyInput ref={ref} placeholder="Enter text" />;
}

在 React 19 内部,当渲染函数组件时,如果组件的 props 中有 ref 字段,React 会直接将其传入,不再需要中间的包装层。forwardRef 在 React 19 中仍然有效(向后兼容),但官方建议逐步迁移到直接 prop 模式。

文档元数据:在组件中声明 title 和 meta

React 19 原生支持在组件中声明文档元数据,无需第三方库:

function BlogPost({ post }) {
  return (
    <article>
      {/* 这些标签会被自动提升到 <head> */}
      <title>{post.title}</title>
      <meta name="description" content={post.summary} />
      <link rel="canonical" href={post.url} />
      
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

React 19 的渲染器会识别 <title>、<meta>、<link> 等特殊标签,并将它们"提升"(hoist)到文档的 <head> 部分。在服务端渲染(SSR)场景下,这些标签会在 HTML 输出时被正确放置在 <head> 中;在客户端渲染场景下,React 会使用 DOM API 将它们插入 <head>。

当同一个元数据标签出现在多个组件中时,React 会进行去重——例如,多个组件都声明了 <title>,页面上只会呈现最后一个(或优先级最高的那个)。

资源预加载 API

React 19 新增了一组资源加载 API,可以在组件中声明式地预加载关键资源:

import { preload, preinit, prefetchDNS, preconnect } from 'react-dom';

function VideoPlayer({ src }) {
  // 预加载视频资源(以更高优先级获取,但不立即执行)
  preload(src, { as: 'video' });
  
  // preinit: 预加载并立即执行(适用于关键脚本和样式)
  preinit('https://cdn.example.com/player.js', { as: 'script' });
  
  return <video src={src} controls />;
}

function App() {
  // DNS 预解析和预连接
  prefetchDNS('https://api.example.com');
  preconnect('https://api.example.com', { crossOrigin: 'anonymous' });
  
  return <VideoPlayer src="/video.mp4" />;
}

这些 API 最终生成 <link rel="preload">、<link rel="prefetch"> 等 HTML 标签,帮助浏览器更早地获取关键资源,改善页面加载性能。在 SSR 场景下,这些提示会被写入初始 HTML,让浏览器在解析 HTML 时就开始并行加载资源。


Level 3 · 规范怎么定义的(资深)

React 18 → 19 迁移指南

破坏性变更

1. 移除的遗留根 API

// 已移除(React 18 已废弃)
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, container);        // ❌
ReactDOM.hydrate(<App />, container);       // ❌
ReactDOM.unmountComponentAtNode(container); // ❌

// 使用新 API
import { createRoot, hydrateRoot } from 'react-dom/client';
const root = createRoot(container);
root.render(<App />);                       // ✅

2. 移除 propTypes 运行时检查

// React 19 完全移除了运行时 propTypes 验证
MyComponent.propTypes = { ... }; // 不再有任何效果,建议改用 TypeScript

3. 移除 defaultProps(函数组件)

// React 19 中,函数组件的 defaultProps 被移除
// 改为使用 ES 默认参数
function Button({ color = 'blue', size = 'medium' }) {
  return <button className={`btn-${color} btn-${size}`}>Click</button>;
}
// 注意:类组件的 defaultProps 仍然保留

4. ref 回调的清理函数

// React 19 中,ref 回调可以返回清理函数
function Component() {
  return (
    <div
      ref={(node) => {
        // setup
        if (node) {
          node.addEventListener('click', handler);
        }
        // cleanup(React 19 新增)
        return () => {
          node.removeEventListener('click', handler);
        };
      }}
    />
  );
}

5. Context 提供方写法简化

// React 18
<ThemeContext.Provider value={theme}>
  {children}
</ThemeContext.Provider>

// React 19 可以直接使用 Context 本身作为 Provider
<ThemeContext value={theme}>
  {children}
</ThemeContext>

迁移建议优先级

优先级 变更 工作量
必须 替换 ReactDOM.render 为 createRoot 低
必须 删除 propTypes 中(建议迁移到 TypeScript)
建议 将 forwardRef 替换为直接 prop 中
建议 将 Context.Provider 替换为 Context 低
可选 使用 use() 替换 useContext 低
可选 采用 Actions 模式替换手动表单状态管理 高(收益大)

React 19 的这些改进不是孤立的功能点,而是围绕一个核心主题展开的:让 React 代码更接近"描述意图"而不是"描述实现"。Actions 让你描述"提交这个表单"而不是管理 pending/error 状态;Compiler 让你描述"这是组件的渲染逻辑"而不是手动优化哪些值应该记忆化;文档元数据让你在组件中声明"这个页面的标题是什么"而不是调用外部库的命令式 API。这是 React 设计哲学的持续演进。


Level 4 · 边界与陷阱(所有人)

改进的错误报告与 hydration 错误提示

更好的错误分组

React 19 改进了错误捕获机制。在之前的版本中,一个错误可能在控制台触发多次输出(react 内部处理一次,window.onerror 再捕获一次)。React 19 统一了错误报告路径:

// React 19 中,新增了两个 root 选项
const root = createRoot(container, {
  // 可恢复的错误(有 ErrorBoundary 捕获)
  onRecoverableError: (error, errorInfo) => {
    console.error('Caught by ErrorBoundary:', error, errorInfo.componentStack);
  },
  // 不可恢复的错误(没有 ErrorBoundary 捕获)
  onUncaughtError: (error, errorInfo) => {
    console.error('Uncaught error:', error, errorInfo.componentStack);
  },
});

Hydration 错误的差异展示

React 19 在 hydration 错误时会显示服务端与客户端渲染结果的具体差异,而不仅仅是"hydration 失败"这样的模糊提示:

Hydration failed because the server rendered HTML didn't match the client.
  Server: <div class="container">Hello</div>
  Client: <div class="container">World</div>

这让定位和修复 SSR 不一致问题变得直接得多。

本章评分
4.5  / 5  (3 评分)

💬 留言讨论