Post

认识 React 19

认识 React 19

Actions:革命性的异步状态管理

告别手动状态管理的恶梦

在 React 19 之前,处理表单提交和异步操作一直是一个痛点。我们需要手动管理加载状态、错误状态和乐观更新,代码冗长且容易出错。

React 18 的传统做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function UpdateProfile() {
  const [name, setName] = useState("");
  const [error, setError] = useState(null);
  const [isPending, setIsPending] = useState(false);

  const handleSubmit = async () => {
    setIsPending(true);
    setError(null);

    try {
      const result = await updateUserProfile(name);
      if (result.error) {
        setError(result.error);
        return;
      }
      // 处理成功逻辑
      redirect("/profile");
    } catch (err) {
      setError(err.message);
    } finally {
      setIsPending(false);
    }
  };

  return (
    <div>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <button onClick={handleSubmit} disabled={isPending}>
        {isPending ? "更新中..." : "更新"}
      </button>
      {error && <p>{error}</p>}
    </div>
  );
}

React 19 的 Actions 方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function UpdateProfile() {
  const [error, submitAction, isPending] = useActionState(
    async (previousState, formData) => {
      const error = await updateUserProfile(formData.get("name"));
      if (error) {
        return error;
      }
      redirect("/profile");
      return null;
    },
    null
  );

  return (
    <form action={submitAction}>
      <input type="text" name="name" />
      <button type="submit" disabled={isPending}>
        {isPending ? "更新中..." : "更新"}
      </button>
      {error && <p>{error}</p>}
    </form>
  );
}

新的 Hooks

useActionState

React 19 引入了 useActionState hook,它接受一个 Action 函数并返回一个包装的 Action 以及相关状态。

1
2
3
4
5
6
7
8
9
10
11
const [error, submitAction, isPending] = useActionState(
  async (previousState, formData) => {
    // 异步操作
    const result = await apiCall(formData);
    if (result.error) {
      return result.error; // 返回错误状态
    }
    return null; // 成功状态
  },
  null // 初始状态
);

useActionState 返回的第一个值是当前 action 的状态,可以用来表示错误信息、成功消息或更复杂的状态对象。

useOptimistic

useOptimistic hook 让你可以在异步操作进行时立即显示乐观的结果。

它在内部其实同时为你管理着两个状态。

  • 真实状态 (Real State):这是最可靠、最权威的状态,通常由服务器提供,通过 props 传递给你的组件。
  • 乐观状态 (Optimistic State):这是一个临时的、“希望”它能成真的状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function TodoList({ todos, addTodo }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (state, newTodo) => [...state, newTodo]
  );

  const submitAction = async (formData) => {
    const newTodo = { id: Date.now(), text: formData.get("todo") };
    addOptimisticTodo(newTodo);

    // 实际的异步操作
    await addTodo(newTodo);
  };

  return (
    <div>
      {optimisticTodos.map(todo => (
        <div key={todo.id}>{todo.text}</div>
      ))}
      <form action={submitAction}>
        <input name="todo" />
        <button type="submit">添加</button>
      </form>
    </div>
  );
}

useFormStatus

useFormStatus 允许设计系统组件访问父表单的状态信息。

1
2
3
4
5
6
7
8
9
10
11
import { useFormStatus } from 'react-dom';

function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? "提交中..." : "提交"}
    </button>
  );
}

useFormStatus 出现之前,我们处理表单提交状态的通用流程是:

  1. 在表单所在的父组件中,定义一个加载状态,例如 const [isPending, setIsPending] = useState(false)
  2. 当用户点击提交按钮时,我们需要手动调用 setIsPending(true)
  3. 然后,把 isPending 这个状态通过属性(prop)一层层传递给需要它的子组件,比如提交按钮 <SubmitButton disabled={isPending} />
  4. 当异步操作(比如网络请求)结束后,再手动调用 setIsPending(false)。 这个过程不仅代码繁琐,更重要的是造成了父组件和子组件的紧密耦合。父组件必须管理所有状态,并显式地将这些状态“告诉”子组件。

useFormStatus 的作用就是为了打破这种耦合。它允许提交按钮这样的子组件,能够自己主动地感知它所在的 <form> 当前是否正在提交,而不再需要父组件通过属性来通知它。

新的 use API:条件式资源读取

React 19 引入了新的 use API,可以在渲染中读取资源如 Promise 和 Context。

读取 Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { use, Suspense } from 'react';

// use API 会 "解包" Promise
// 如果在 pending,它会暂停渲染,并向上寻找 Suspense
// 如果 reject,它会抛出错误,并向上寻找 ErrorBoundary
function Comments({ commentsPromise }) {
  // use 会暂停直到 promise 解析
  const comments = use(commentsPromise);

  // 组件只关心成功后的渲染逻辑
  return comments.map(comment => (
    <div key={comment.id}>{comment.text}</div>
  ));
}

function BlogPost({ commentsPromise }) {
  return (
    <ErrorBoundary fallback={<p>⚠️ Something went wrong while fetching the post!</p>}>
      <Suspense fallback={<div>加载评论中...</div>}>
        <Comments commentsPromise={commentsPromise} />
      </Suspense>
    </ErrorBoundary>
  );
}

条件式 Context 读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { use } from 'react';

function Heading({ children }) {
  if (children == null) {
    return null;
  }

  // 这在 useContext 中是不可能的,因为有早期返回
  const theme = use(ThemeContext);

  return (
    <h1 style={ { color: theme.color } }>
      {children}
    </h1>
  );
}

useContext 仍然是读取 Context 的主要方式。use 提供了一种在渲染期间(包括在条件语句和循环中)读取资源的补充能力,这使得 useContext 的使用场景更加纯粹。

Server Components:全栈架构的完善

什么是 Server Components?

Server Components 是一种新的组件类型,可以在服务器上提前渲染,与客户端应用程序或 SSR 服务器分离。

Server Components 的优势:

  • 零客户端 Bundle 大小:服务器组件不会被打包到客户端
  • 直接访问服务器资源:可以直接读取数据库、文件系统等
  • 自动代码分割:只有需要的客户端代码才会被发送
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ServerComponent.server.js
import { db } from './database';

export default async function ServerComponent() {
  // 直接在服务器上访问数据库
  const posts = await db.posts.findMany();

  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
        </article>
      ))}
    </div>
  );
}

Server Actions

Server Actions 允许客户端组件调用在服务器上执行的异步函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// actions.js
'use server';

export async function createPost(formData) {
  const title = formData.get('title');
  const content = formData.get('content');

  // 服务器端操作
  const post = await db.posts.create({
    data: { title, content }
  });

  return post;
}

// ClientComponent.js
import { createPost } from './actions';

export default function CreatePost() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="标题" />
      <textarea name="content" placeholder="内容" />
      <button type="submit">创建文章</button>
    </form>
  );
}

其它改进

ref 作为 prop

从 React 19 开始,你可以直接在函数组件中访问 ref 作为 prop。

1
2
3
4
5
6
7
8
9
// React 19 之前需要 forwardRef
const MyInput = forwardRef(({ placeholder }, ref) => {
  return <input placeholder={placeholder} ref={ref} />;
});

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

Context 作为 Provider

React 19 允许直接渲染 Context 作为 Provider。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const ThemeContext = createContext('light');

// React 19 之前
function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Header />
    </ThemeContext.Provider>
  );
}

// React 19
function App() {
  return (
    <ThemeContext value="dark">
      <Header />
    </ThemeContext>
  );
}

文档元数据支持

React 19 添加了对在组件中渲染文档元数据标签的本地支持。

1
2
3
4
5
6
7
8
9
10
11
12
13
function BlogPost({ post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      <title>{post.title}</title>
      <meta name="description" content={post.excerpt} />
      <meta name="author" content={post.author} />
      <link rel="canonical" href={`/posts/${post.slug}`} />

      <div>{post.content}</div>
    </article>
  );
}

样式表支持

React 19 为样式表提供了更深度的集成,支持 precedence 属性来管理样式表的插入顺序。

1
2
3
4
5
6
7
8
9
10
11
function Component() {
  return (
    <Suspense fallback="加载中...">
      <link rel="stylesheet" href="critical.css" precedence="high" />
      <link rel="stylesheet" href="main.css" precedence="default" />
      <div className="styled-content">
        内容
      </div>
    </Suspense>
  );
}

升级指南

安装 React 19

1
npm install react@19 react-dom@19

主要破坏性变更

  1. 新的 JSX Transform 是必需的

    • 需要更新 Babel 或 TypeScript 配置
  2. 去除了一些过时的 API

    • React.Children.toArray 的行为改变
    • 一些 legacy 的 ref 处理方式
  3. 严格模式的增强

    • 开发模式下更严格的检查

升级步骤

  1. 更新依赖

    1
    2
    
    npm install react@19 react-dom@19
    npm install @types/react@19 @types/react-dom@19  # TypeScript 项目
    
  2. 更新 Root API(如果还在使用旧版本)

    1
    2
    3
    4
    5
    
    // 确保使用 createRoot
    import { createRoot } from "react-dom/client";
    
    const root = createRoot(document.getElementById("root"));
    root.render(<App />);
    
  3. 逐步采用新特性

    • 开始在新组件中使用 Actions
    • 将复杂的异步状态管理迁移到 useActionState
    • 考虑使用 Server Components(如果适用)

性能提升

React 19 在性能方面有显著改进:

  • Actions 的自动批处理减少了不必要的重新渲染
  • Server Components显著减少了客户端 Bundle 大小
  • 改进的 Suspense提供了更好的用户体验
  • 优化的 hydration处理第三方脚本和浏览器扩展

实际应用建议

适合立即使用的特性

  1. Actions + useActionState:适合所有表单处理
  2. ref 作为 prop:简化组件设计
  3. 文档元数据:改善 SEO 和用户体验

需要谨慎考虑的特性

  1. Server Components:需要支持的框架(Next.js 15+、Remix 等)
  2. use API:需要重新思考数据获取模式

迁移策略

  1. 新项目:直接使用 React 19 和新特性
  2. 现有项目
    • 先升级到 React 19
    • 逐步迁移表单处理到 Actions
    • 考虑长期的 Server Components 采用

总结

React 19 是一个重要的里程碑版本,它不仅带来了 Actions 这样的革命性特性,还完善了全栈架构的支持

  1. Actions 简化了复杂的异步状态管理,让表单处理变得优雅
  2. Server Components 为性能优化开辟了新路径
  3. 现代化的 API 设计减少了样板代码
  4. 更好的开发体验和错误处理
This post is licensed under CC BY 4.0 by the author.