关于 safe-action-hooks
为 React 客户端组件调用 next-safe-action 操作提供 Hooks,并管理结果与状态。不同 Hook 分别适配普通 action 和 stateAction,也支持覆盖式或排队累积式乐观更新。
主要功能
- 01提供四种 action 与 stateAction Hooks
- 02跟踪执行状态和结果
- 03支持乐观状态更新
- 04排队处理累积型更改
- 05初始化结果与重置
适用场景
- 01从交互界面触发操作
- 02通过 stateAction 提交 FormData
- 03乐观替换显示状态
- 04依序应用多个重叠更改
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ next-safe-action/ skills --skill safe-action-hooks安装后在 Claude Code 中输入/safe-action-hooks确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
optimistic-updates.mdstatus-callbacks.mdthrow-on-navigation.mduse-action.mduse-optimistic-state-action.mduse-state-action.md详细介绍
next-safe-action React Hooks
导入
// All hooks
import {
useAction,
useOptimisticAction,
useStateAction,
useOptimisticStateAction,
} from "next-safe-action/hooks";
// Backward-compatible re-export (same useStateAction hook)
import { useStateAction } from "next-safe-action/stateful-hooks";
选择哪个 Hook
| Hook | Action 方法 | 适用场景 |
|---|---|---|
useAction | .action() | 以编程方式触发、交互式 UI、大多数场景 |
useOptimisticAction | .action() | 立即更新会替换状态的 UI(最后写入优先) |
useStateAction | .stateAction() | <form action={formAction}>、服务端的 prevResult、排队的分发 |
useOptimisticStateAction | .stateAction() | 立即更新会累积的 UI:重叠写入会排队,每次都基于已确认的状态进行折叠 |
首先根据 Action 类型选择:.action() 对应 useAction / useOptimisticAction,.stateAction() 对应 useStateAction / useOptimisticStateAction。
useAction — 快速入门
"use client";
import { useAction } from "next-safe-action/hooks";
import { createUser } from "@/app/actions";
export function CreateUserForm() {
const { execute, result, status, isExecuting, isPending } = useAction(createUser, {
onSuccess: ({ data }) => {
console.log("User created:", data);
},
onError: ({ error }) => {
console.error("Failed:", error.serverError);
},
});
return (
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
execute({ name: formData.get("name") as string });
}}>
<input name="name" required />
<button type="submit" disabled={isPending}>
{isPending ? "Creating..." : "Create User"}
</button>
{result.serverError && <p className="error">{result.serverError}</p>}
{result.data && <p className="success">Created: {result.data.id}</p>}
</form>
);
}
useOptimisticAction — 快速入门
"use client";
import { useOptimisticAction } from "next-safe-action/hooks";
import { toggleTodo } from "@/app/actions";
export function TodoItem({ todo }: { todo: Todo }) {
const { execute, optimisticState } = useOptimisticAction(toggleTodo, {
currentState: todo,
updateFn: (state, input) => ({
...state,
completed: !state.completed,
}),
});
return (
<label>
<input
type="checkbox"
checked={optimisticState.completed}
onChange={() => execute({ todoId: todo.id })}
/>
{todo.title}
</label>
);
}
useOptimisticStateAction — 快速入门
适用于必须累积的重叠写入(例如,重新排序一个项目后,在第一次保存完成前再次重新排序)。分发会排队:每次都要等上一次完成,然后服务端会将最后确认的状态作为 prevResult 接收。
"use client";
import { useOptimisticStateAction } from "next-safe-action/hooks";
import { moveItem } from "@/app/actions";
import { reorder } from "@/lib/reorder"; // the same reducer the server runs
export function ReorderList({ items }: { items: Item[] }) {
const { execute, optimisticState, isPending } = useOptimisticStateAction(moveItem, {
currentState: items, // must be a stable reference — compared by identity
updateFn: reorder,
});
return (
<ul data-saving={isPending}>
{optimisticState.map((item) => (
<li key={item.id}>
{item.label}
<button onClick={() => execute({ id: item.id, direction: "up" })}>Up</button>
</li>
))}
</ul>
);
}
Action 必须是 .stateAction(),并且必须重新验证页面所呈现的状态:
"use server";
export const moveItem = actionClient
.inputSchema(moveSchema)
.stateAction(async ({ parsedInput }, { prevResult }) => {
const next = reorder(prevResult.data!, parsedInput); // always the last confirmed state
await db.items.save(next);
revalidatePath("/items");
return next;
});
请参阅 深入了解 useOptimisticStateAction,了解决策规则、待处理更改列表的结构以及注意事项。
useStateAction — 快速入门
"use client";
import { useStateAction } from "next-safe-action/hooks";
import { submitFeedback } from "@/app/actions";
export function FeedbackForm() {
const { formAction, result, isPending, hasSucceeded } = useStateAction(submitFeedback, {
onSuccess: ({ data }) => {
console.log("Submitted:", data);
},
onError: ({ error }) => {
console.error("Failed:", error.serverError);
},
});
return (
<form action={formAction}>
<input name="rating" type="number" min="1" max="5" required />
<textarea name="comment" required />
<button type="submit" disabled={isPending}>
{isPending ? "Submitting..." : "Submit"}
</button>
{result.validationErrors?.comment && (
<p className="error">{result.validationErrors.comment._errors[0]}</p>
)}
{hasSucceeded && <p className="success">Thank you!</p>}
</form>
);
}
服务端 Action 必须使用 .stateAction()(而不是 .action())。<form action={formAction}> 会提交原始 FormData,因此输入架构必须能解析 FormData(例如,使用 zod-form-data):
"use server";
import { z } from "zod";
import { zfd } from "zod-form-data";
import { actionClient } from "@/lib/safe-action";
export const submitFeedback = actionClient
.inputSchema(
zfd.formData({
rating: zfd.numeric(z.number().min(1).max(5)),
comment: zfd.text(z.string()),
})
)
.stateAction(async ({ parsedInput }, { prevResult }) => {
// prevResult contains the previous SafeActionResult
await db.feedback.create({ data: parsedInput });
return { rating: parsedInput.rating };
});
返回值
所有钩子(useAction、useOptimisticAction、useStateAction、useOptimisticStateAction)均返回:
| 属性 | 类型 | 描述 |
|---|---|---|
execute(input) | (input) => void | 发起即忘的执行 |
executeAsync(input) | (input) => Promise<Result> | 返回包含结果的 Promise |
input | Input | undefined | 最近一次派发的输入(通过 execute、executeAsync 或 formAction) |
result | SafeActionResult | 最近一次操作结果——由 4 个分支组成的可辨识联合(idle / success / serverError / validationErrors);检查 status 或任意 has* 简写时会收窄类型 |
reset() | () => void | 将客户端状态重置为初始状态(如果提供了 initResult,则恢复该值),并忽略任何正在执行的操作的结果。它不会取消已经发出的服务端调用。对于排队钩子(useStateAction、useOptimisticStateAction),它还会跳过所有仍在等待执行的派发:对应操作不会运行,也不会发生写入 |
status | HookActionStatus | 当前状态字符串 |
isIdle | boolean | 尚未开始执行 |
isExecuting | boolean | 操作 Promise 正在等待完成 |
isTransitioning | boolean | React transition 正在等待完成 |
isPending | boolean | isExecuting || isTransitioning,但调用 reset() 后除外:即使它打断的、无法取消的 transition 仍在完成过程中,重置也会立即报告 idle |
hasSucceeded | boolean | 最近一次执行已完成且没有错误(无返回值的操作成功时,result.data 仍为 undefined) |
hasErrored | boolean | 最近一次执行出现了 serverError、validationErrors,或抛出了异常(直接抛出的异常会使 result 为空) |
hasNavigated | boolean | 最近一次执行触发了导航 |
useOptimisticAction 还会返回:
| optimisticState | State | 乐观更新后的状态 |
useStateAction 还会返回:
| formAction | (input) => void | <form action={formAction}> 模式的派发器 |
useOptimisticStateAction 返回 useStateAction 的所有内容,另外还会返回:
| optimisticState | State | 与所有正在执行的更改合并后的已确认状态(始终有定义) |
钩子本身的返回值也是一个可辨识联合,由 status 以及每个 has* / is* 简写作为判别键(每个分支中的类型均为字面量 true / false)。收窄任意判别键都会收窄 result——例如,在 if (hasSucceeded) 中,result.data 的类型为 Data(而非 Data | undefined)。请参阅通过钩子状态收窄类型。
initResult 选项
所有钩子都接受 initResult,用于以预加载结果(例如从服务端获取的数据)初始化钩子:对于 useAction / useStateAction,在 opts 对象中提供;对于 useOptimisticAction / useOptimisticStateAction,在 utils 对象中提供(与 currentState / updateFn 并列)。该值仅在挂载时捕获一次(类似于 React 的 useActionState 初始状态):之后对该选项的更改会被忽略,reset() 会恢复挂载时的值。预置值的形状会精确决定 idle 分支中 result 的类型。请参阅深入了解 initResult。
相关文档
- execute 与 executeAsync、结果处理
- 深入了解 useStateAction(决策表、formAction)
- 使用 useOptimisticAction 进行乐观更新
- useOptimisticStateAction:针对重叠写入的排队乐观更新
- 状态生命周期及所有回调
- throwOnNavigation 标志
反模式
// BAD: Using executeAsync without try/catch when navigation errors are possible
const handleClick = async () => {
const result = await executeAsync({ id }); // Throws on redirect!
showToast(result.data);
};
// GOOD: Wrap executeAsync in try/catch
const handleClick = async () => {
try {
const result = await executeAsync({ id });
showToast(result.data);
} catch (e) {
// Handle non-navigation errors here if needed, then re-throw
// Navigation errors must propagate to Next.js
throw e;
}
};
// BAD: Last-write-wins for changes that accumulate — the second move builds on stale state,
// and the response of the first move is discarded
const { execute } = useOptimisticAction(moveItem, { currentState: items, updateFn: reorder });
// GOOD: Queued dispatches, each folding over the previous result
const { execute } = useOptimisticStateAction(moveItem, { currentState: items, updateFn: reorder });
// BAD: Using .action() with useStateAction — type error
const myAction = actionClient.inputSchema(schema).action(async ({ parsedInput }) => { ... });
useStateAction(myAction); // TypeScript error!
// GOOD: Use .stateAction() for useStateAction
const myAction = actionClient.inputSchema(schema).stateAction(async ({ parsedInput }, { prevResult }) => { ... });
useStateAction(myAction); // Works!