讲解

React 19(2024 年 12 月稳定)的主题是「把异步变简单」。最核心的新概念是 Action:一个传给表单的异步函数,React 自动管理它的 pending 状态、错误和乐观更新。配合三个新 Hook:

useActionState——表单状态机:const [error, submitAction, isPending] = useActionState(fn, null),fn 接收 (上一个状态, 表单数据 FormData),返回的新状态自动更新,提交期间 isPending 为真。以前要手写三四个 useState 的「提交中/成功/失败」逻辑,现在一个 Hook 收编。

useOptimistic——乐观更新:const [optimisticTodos, addOptimistic] = useOptimistic(todos),提交时立刻把新项显示出来,请求失败自动回滚到真实状态。点赞、发送消息这类「用户不想等」的交互,体验从「转圈一秒」变成「瞬时响应」。

use——可以在渲染期间读 Promise 和 Context 的新 Hook(它是唯一能放进条件里的 Hook):const data = use(promise),配合 Suspense 显示加载态。它把「Promise 结果」变成了可以同步书写的值,是 React 服务端组件和客户端数据加载的统一接口。

其他值得知道的:ref 现在可以直接作为普通 prop 传递(forwardRef 退出历史舞台);useFormStatus 让深层子组件(比如提交按钮)不用 props 就能拿到所在表单的提交状态;文档元数据(title/meta)可以直接在组件里渲染。升级建议:新项目直接用 19 的写法,老项目这些特性都是增量式采纳,不用重写。

示例

模拟 useActionState 的表单状态机和 useOptimistic 的回滚(在本教程构建时被真实执行):

import assert from 'node:assert/strict';

// 迷你 useActionState:pending 自动管理,返回值成为新状态
async function simulateAction(actionFn, formData, failTimes = 0) {
  let state = null;
  let pending = false;
  let attempts = 0;
  async function dispatch(data) {
    pending = true;
    attempts++;
    try {
      state = await actionFn(state, data, attempts <= failTimes);
    } finally {
      pending = false;
    }
    return { state, pending };
  }
  pending = true;
  const result = await dispatch(formData);
  return result;
}

// 注册 action:返回错误信息或 null(成功)
async function register(prev, formData, shouldFail) {
  if (shouldFail) throw new Error('network');
  if (!formData.name) return '姓名不能为空';
  if (formData.name.length < 2) return '姓名至少 2 个字';
  return null;
}

const bad = await simulateAction(register, { name: '小' });
assert.strictEqual(bad.state, '姓名至少 2 个字');
assert.strictEqual(bad.pending, false); // 结束后 pending 自动复位

const good = await simulateAction(register, { name: '小明' });
assert.strictEqual(good.state, null);

// 迷你 useOptimistic:乐观展示,失败回滚
function createOptimistic(realState) {
  let optimistic = realState;
  return {
    get value() {
      return optimistic;
    },
    add(item, confirm) {
      optimistic = [...realState, item]; // 立刻显示
      return confirm()
        .then(() => {
          realState = [...realState, item]; // 成功:写进真实状态
          optimistic = realState;
        })
        .catch(() => {
          optimistic = realState; // 失败:回滚
        });
    },
  };
}

const likes = createOptimistic([]);
await likes.add('小明赞了', () => Promise.reject(new Error('网络抖动')));
assert.deepStrictEqual(likes.value, []); // 自动回滚,界面回到真实状态
await likes.add('小红赞了', () => Promise.resolve());
assert.deepStrictEqual(likes.value, ['小红赞了']);

console.log('表单校验失败状态:', bad.state, '| pending 自动复位:', bad.pending === false);
console.log('乐观更新: 失败自动回滚,成功后真实状态:', likes.value.join('、'));

React 19 的真实写法:

import { useActionState } from 'react';

function SignupForm() {
  const [error, submitAction, isPending] = useActionState(async (prev, formData) => {
    const name = formData.get('name');
    if (name.length < 2) return '姓名至少 2 个字';
    await fetch('/api/signup', { method: 'POST', body: formData });
    return null; // 成功
  }, null);

  return (
    <form action={submitAction}>
      <input name="name" placeholder="姓名" />
      {error && <em>{error}</em>}
      <button disabled={isPending}>{isPending ? '提交中…' : '注册'}</button>
    </form>
  );
}

常见坑

  • 老代码里继续 forwardRef:React 19 里 ref 是普通 prop,新代码直接 function Input({ ref, ... }) 接收。
  • use() 当普通 Hook 乱用:use 可以进条件,但读的 Promise 应该是稳定引用(每次渲染新建 Promise 会无限挂起)。
  • Action 里手动管 pending:useActionState 已自动管理 isPending,再写 useState 是重复劳动。
  • 乐观更新不做失败处理:useOptimistic 的回滚是自动的,但你要设计「失败后用户能感知」的提示。
  • 盲目升级老项目:React 19 对水合错误信息等有破坏性变更,升级前看官方升级指南并跑全测试。

小结

React 19 = Actions(useActionState 管表单状态机)+ useOptimistic(乐观更新自动回滚)+ use()(渲染期读 Promise);ref 变普通 prop。下一章看异步 UI 的编排:Suspense。