深入源码:React 19 useActionState 与 Next.js Server Actions 的完美融合

本文深入探讨 React 19 的新 Hook useActionState(原 useFormState),结合 Next.js 源码分析其在 Progressive Enhancement(渐进增强)和 Server Actions 中的核心机制。

前言

在 React 19 中,useActionState 是一个非常重要的新 Hook,它专门用于处理 Form Actions 的状态。如果你之前使用过 useFormState,其实它们是同一个东西,只是名字变了。

它的核心作用是:允许你在 Form Action 提交时,获取服务器返回的状态(如错误信息、成功提示),并根据这个状态更新 UI。

但更重要的是,当它与 Next.js App Router 结合时,它展现出了强大的渐进增强(Progressive Enhancement)能力------即使 JavaScript 被禁用,表单依然可以正常提交并更新状态。

今天我们就通过源码分析,看看 Next.js 是如何实现这一点的。

1. 实战:useActionState 的基本用法

首先,我们看一个简单的 Todo List 例子。这个例子来源于 Next.js 官方示例 next-forms

Server Action (app/actions.ts)

这是运行在服务端的代码,负责处理数据库操作。

typescript 复制代码
"use server";

import { revalidatePath } from "next/cache";
import postgres from "postgres";
import { z } from "zod";

// ...数据库连接代码...

export async function createTodo(
  prevState: { message: string },
  formData: FormData,
) {
  const schema = z.object({
    todo: z.string().min(1),
  });
  
  const parse = schema.safeParse({
    todo: formData.get("todo"),
  });

  if (!parse.success) {
    return { message: "Failed to create todo" };
  }

  const data = parse.data;

  try {
    await sql`
      INSERT INTO todos (text)
      VALUES (${data.todo})
    `;

    revalidatePath("/");
    return { message: `Added todo ${data.todo}` };
  } catch (e) {
    return { message: "Failed to create todo" };
  }
}

注意 createTodo 的签名:它接收 prevState 作为第一个参数。这就是 useActionState 注入的状态。

Client Component (app/add-form.tsx)

这是客户端组件,使用 useActionState 绑定 Server Action。

typescript 复制代码
"use client";

import { useActionState } from "react";
import { useFormStatus } from "react-dom";
import { createTodo } from "@/app/actions";

const initialState = {
  message: "",
};

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

  return (
    <button type="submit" aria-disabled={pending}>
      Add
    </button>
  );
}

export function AddForm() {
  // useActionState 接收 action 和初始状态
  // 返回最新的 state 和一个新的 dispatch action
  const [state, formAction] = useActionState(createTodo, initialState);

  return (
    <form action={formAction}>
      <label htmlFor="todo">Enter Task</label>
      <input type="text" id="todo" name="todo" required />
      <SubmitButton />
      <p aria-live="polite" className="sr-only" role="status">
        {state?.message}
      </p>
    </form>
  );
}

这个模式非常优雅:

  1. 初始状态message 为空。
  2. 提交 :用户点击按钮,formAction 触发 createTodo
  3. 更新 :Server Action 执行完毕返回新对象(如 { message: "Added todo..." }),state 自动更新,UI 重新渲染。

2. 核心优势一:无 JS 支持 (Progressive Enhancement)

大家觉得,如果我禁用了浏览器的 JavaScript,这个表单还能工作吗?

答案是:可以!

看看这个效果(JS 被禁用):

虽然 JS 加载失败,但提交表单依然可以更新页面:

原理分析:MPA 模式

当 JS 被禁用时,<form> 会退化为标准的 HTML 表单提交。浏览器会发送一个 POST 请求到当前 URL。

Next.js 的服务器端处理逻辑主要在 packages/next/src/server/app-render/action-handler.ts 中。

1. 识别请求类型

Next.js 会检查请求是否是 Server Action。有趣的是,它会区分 Fetch Action (有 JS,AJAX 请求)和 MPA Action(无 JS,表单 POST)。

next.js/packages/ne...

typescript 复制代码
// next.js/packages/next/src/server/app-render/action-handler.ts

// ...
  const {
    actionId,
    isMultipartAction,
    isFetchAction, // 关键点:是否有 Next-Action 头或相关标识
    // ...
  } = getServerActionRequestMetadata(req)
// ...

2. 处理 MPA Action

如果 isFetchAction 为 false(即无 JS 环境),Next.js 会走另一条路:

next.js/packages/ne...

typescript 复制代码
// Multipart POST, but not a fetch action.
// Potentially an MPA action, we have to try decoding it to check.

const action = await decodeAction(formData, serverModuleMap)
if (typeof action === 'function') {
  // 这是一个 MPA action (无 JS)
  
  // 执行 Action
  const { actionResult } = await executeActionAndPrepareForRender(
    action,
    [], // MPA 模式下没有额外的绑定参数
    workStore,
    requestStore,
    actionWasForwarded
  )

  // 关键:解码 Form State
  const formState = await decodeFormState(
    actionResult,
    formData,
    serverModuleMap
  )

  // Skip the fetch path.
  // We need to render a full HTML version of the page for the response
  return {
    type: 'done',
    result: undefined, // 这里的 undefined 意味着后续会继续进行页面渲染 (App Render)
    formState, // 把 Action 的执行结果传递给页面渲染流程
  }
}

核心流程:

  1. 解析 FormData :Next.js 从 POST body 中解析出 actionId(这是一个隐藏字段,React 生成的)。
  2. 执行 Action:找到对应的 Server Action 函数并执行。
  3. 携带 State 重新渲染 :它不会 返回 JSON,而是带着 formState(Action 的返回值)重新渲染整个页面(Full Page Render)。
  4. HTML 返回:浏览器收到的是一个新的 HTML 页面,其中包含了更新后的 UI 和数据。

这就是为什么禁用 JS 也能工作!Next.js 自动帮我们处理了"回退到传统表单提交"的所有逻辑。

3. 核心优势二:客户端交互与局部更新

当 JS 启用时,React 会接管表单提交。

客户端劫持 (app-call-server.ts)

当表单提交时,Next.js 的客户端逻辑会拦截它。注意,这里使用了 React 的 startTransition(参考:深入理解 React 的 startTransition)来标记非紧急更新,保持 UI 响应性:

源码链接:packages/next/src/client/app-call-server.ts#L5

typescript 复制代码
export async function callServer(actionId: string, actionArgs: any[]) {
  return new Promise((resolve, reject) => {
    startTransition(() => {
      dispatchAppRouterAction({
        type: ACTION_SERVER_ACTION,
        actionId,
        actionArgs,
        resolve,
        reject,
      })
    })
  })
}

发送请求 (server-action-reducer.ts)

请求会通过 fetchServerAction 发送。注意,这次是 fetch 请求,且带有特殊的 Header:

源码链接:packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts#L96

typescript 复制代码
async function fetchServerAction(/*...*/) {
  // ...
  const headers: Record<string, string> = {
    Accept: RSC_CONTENT_TYPE_HEADER, // 告诉服务器我要 RSC Payload (Flight Data)
    [ACTION_HEADER]: actionId,       // Next-Action: <id>
    [NEXT_ROUTER_STATE_TREE_HEADER]: prepareFlightRouterStateForRequest(state.tree),
    // ...
  }
  
  const res = await fetch(state.canonicalUrl, { method: 'POST', headers, body })
  // ...
}

优势

  1. 局部更新:服务器返回的是 RSC Payload(Diff),React 只更新变动的部分,页面不刷新,滚动条位置保留。
  2. Pending 状态useFormStatus 可以立即读取 pending 状态,展示 Loading 动画。这是传统 HTML 表单做不到的。

4. 核心优势三:Server-Side UI Updates (RSC 集成)

这是 useActionState + Next.js 最强大的地方。

传统的 API 请求流程是: Client 请求 -> Server 处理 -> Server 返回 JSON -> Client 解析 JSON -> Client 更新 State -> Client 重新渲染组件

Next.js Server Actions 的流程是: Client 请求 -> Server 处理 -> Server 重新渲染组件 (RSC) -> Server 返回 UI 补丁 (Flight Data) -> Client 应用补丁

源码验证

action-handler.ts 中,当 Action 执行完后:

源码链接:packages/next/src/server/app-render/action-handler.ts#L1081

typescript 复制代码
// ... Action 执行完毕 ...

// For form actions, we need to continue rendering the page.
if (isFetchAction) {
  return {
    type: 'done',
    result: await generateFlight(req, ctx, requestStore, {
      actionResult: Promise.resolve(actionResult),
      // ...
    }),
  }
}

generateFlight 会生成 React Server Component 的序列化数据。

这意味着,如果在 Action 中调用了 revalidatePath('/')

  1. Next.js 会在服务器上重新渲染首页。
  2. 你的 todos 列表(从数据库读取)会在服务器端更新。
  3. 新的 UI 结构(包含新添加的 todo)会被发送给客户端。
  4. 客户端直接合并 DOM,不需要在客户端写一行 todos.push(newTodo) 的状态管理代码!

Form 状态与 UI 更新同步useActionState 拿到了 message,而页面列表通过 RSC 更新了。

5. 其他优势

安全性 (CSRF 自动防护)

action-handler.ts 中,Next.js 内置了 Origin 检查,防止 CSRF 攻击:

next.js/packages/ne...

typescript 复制代码
// next.js/packages/next/src/server/app-render/action-handler.ts
// This is to prevent CSRF attacks. 
if (!originDomain) {
  // ... warning
} else if (!host || originDomain !== host.value) {
  if (isCsrfOriginAllowed(originDomain, serverActions?.allowedOrigins)) {
    // Allowed
  } else {
    // Block attack
    console.error(`... Aborting the action.`)
    // ...
  }
}

简化代码

对比一下传统做法:

  • 传统onSubmit, e.preventDefault(), fetch('/api/...'), await res.json(), if (res.ok) ... else ...,还要处理 Loading。
  • Next.js :一个 action 属性搞定。错误处理和 Loading 状态都通过 Hook 自动管理。

总结

useActionState 不仅仅是一个状态管理 Hook,在 Next.js 中,它是连接客户端表单与服务端逻辑的桥梁。

它的优势在于:

  1. 渐进增强:JS 挂了?没问题,MPA 模式顶上。
  2. 开发体验:像写普通函数一样写后端逻辑,不需要手动 fetch。
  3. RSC 融合:一次请求,同时完成"状态更新"和"UI 重新渲染"。
  4. 性能优化 :底层使用 startTransition,保持 UI 响应性,避免阻塞渲染。
  5. 安全可靠:内置 CSRF 防护。

下次写表单时,试试 useActionState,感受一下现代 React 开发的魅力吧!

相关推荐
ErMao4 小时前
开始搭建第一个React项目吧~
前端·react.js
苹果电脑的鑫鑫5 小时前
vue和react缩进规则的配置项如何配置
前端·vue.js·react.js
yuhaiqun19897 小时前
学AI Agent:从React模式到Plan框架,3条路径一次学透
人工智能·经验分享·笔记·react.js·机器学习·ai·aigc
程序员笨鸟7 小时前
[特殊字符] React 高频 useEffect 导致页面崩溃的真实案例:从根因排查到彻底优化
前端·javascript·学习·react.js·面试·前端框架
普通网友7 小时前
框架适配:React/Vue 项目中如何高效使用 debugger 断点
javascript·vue.js·react.js
Shriley_X7 小时前
React
javascript·react.js·ecmascript
Highcharts.js7 小时前
从旧版到新版:Highcharts for React 迁移全攻略 + 开发者必知的 5 大坑
前端·react.js·前端框架·编辑器·highcharts
独角鲸网络安全实验室7 小时前
高危预警!React核心组件曝CVSS 9.8漏洞,数百万开发者面临远程代码执行风险
运维·前端·react.js·网络安全·企业安全·漏洞·cve-2025-11953
西瓜凉了半个夏~7 小时前
React专题:react,redux以及react-redux常见一些面试题
前端·javascript·react.js