深入源码: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 开发的魅力吧!

相关推荐
古茗前端团队3 小时前
由 key 引发的思考
前端·javascript·react.js
用户298698530143 天前
React 前端将 Excel 工作表导出为图片的技术实践
javascript·react.js·excel
終不似少年遊*4 天前
【安全业务Agent】从场景到编排
python·react.js·ai·langchain·agent·安全架构·工作流
JavaPub-rodert5 天前
从零搭建通用管理后台:ShiyuAdmin 的 Go、React 与 RBAC 实践
开发语言·react.js·golang
Highcharts.js5 天前
React 中原生支持使用 JSX 组件配置自定义表格 Highcharts Grid
前端·javascript·react.js·前端框架·jsx·highcharts·自定义表格
JudithHuang5 天前
React 嵌套路由与布局
前端·react.js·arcgis·react·react router
梯度下降者5 天前
3天时间,我使用 Cursor 开发了一个提示词生成器
ai编程·next.js·cursor·产品开发·提示词生成器
前端小D5 天前
React Native项目开发(iOS侧为例)
javascript·react native·react.js
志尊宝6 天前
AI原生前端开发(042):AI辅助的路由设计与优化
javascript·react.js·ai-native
znsoft7 天前
MaClaw(码卡龙)技术拆解:一个 Go + React 的可自进化智能体平台
开发语言·react.js·golang