本文深入探讨 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>
);
}
这个模式非常优雅:
- 初始状态 :
message为空。 - 提交 :用户点击按钮,
formAction触发createTodo。 - 更新 :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)。
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 会走另一条路:
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 的执行结果传递给页面渲染流程
}
}
核心流程:
- 解析 FormData :Next.js 从 POST body 中解析出
actionId(这是一个隐藏字段,React 生成的)。 - 执行 Action:找到对应的 Server Action 函数并执行。
- 携带 State 重新渲染 :它不会 返回 JSON,而是带着
formState(Action 的返回值)重新渲染整个页面(Full Page Render)。 - 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 })
// ...
}
优势
- 局部更新:服务器返回的是 RSC Payload(Diff),React 只更新变动的部分,页面不刷新,滚动条位置保留。
- 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('/'):
- Next.js 会在服务器上重新渲染首页。
- 你的
todos列表(从数据库读取)会在服务器端更新。 - 新的 UI 结构(包含新添加的 todo)会被发送给客户端。
- 客户端直接合并 DOM,不需要在客户端写一行
todos.push(newTodo)的状态管理代码!
Form 状态与 UI 更新同步 :useActionState 拿到了 message,而页面列表通过 RSC 更新了。
5. 其他优势
安全性 (CSRF 自动防护)
在 action-handler.ts 中,Next.js 内置了 Origin 检查,防止 CSRF 攻击:
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 中,它是连接客户端表单与服务端逻辑的桥梁。
它的优势在于:
- 渐进增强:JS 挂了?没问题,MPA 模式顶上。
- 开发体验:像写普通函数一样写后端逻辑,不需要手动 fetch。
- RSC 融合:一次请求,同时完成"状态更新"和"UI 重新渲染"。
- 性能优化 :底层使用
startTransition,保持 UI 响应性,避免阻塞渲染。 - 安全可靠:内置 CSRF 防护。
下次写表单时,试试 useActionState,感受一下现代 React 开发的魅力吧!