Remix Action基础 #
一、Action概述 #
Action 是 Remix 中用于处理数据变更的函数。当表单提交时,action 函数在服务端执行,处理数据修改操作。
二、基本用法 #
2.1 定义Action #
tsx
import type { ActionFunctionArgs } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { Form, useActionData } from "@remix-run/react";
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const title = formData.get("title");
const content = formData.get("content");
await createPost({ title, content });
return redirect("/posts");
}
export default function NewPost() {
return (
<Form method="post">
<div>
<label>标题</label>
<input name="title" type="text" required />
</div>
<div>
<label>内容</label>
<textarea name="content" required />
</div>
<button type="submit">发布</button>
</Form>
);
}
2.2 Action参数 #
Action 函数接收与 loader 相同的参数:
tsx
export async function action({
params, // 路由参数
request, // Request对象
context, // 应用上下文
}: ActionFunctionArgs) {
// ...
}
2.3 获取表单数据 #
tsx
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const title = formData.get("title");
const content = formData.get("content");
const tags = formData.getAll("tags");
return json({ title, content, tags });
}
三、表单提交 #
3.1 Form组件 #
Remix 的 Form 组件是原生表单的增强版:
tsx
import { Form } from "@remix-run/react";
export default function Contact() {
return (
<Form method="post">
<input name="name" />
<input name="email" type="email" />
<textarea name="message" />
<button type="submit">发送</button>
</Form>
);
}
3.2 表单方法 #
tsx
// POST请求(创建)
<Form method="post">
{/* ... */}
</Form>
// PUT请求(更新)
<Form method="put">
{/* ... */}
</Form>
// PATCH请求(部分更新)
<Form method="patch">
{/* ... */}
</Form>
// DELETE请求(删除)
<Form method="delete">
{/* ... */}
</Form>
3.3 处理不同方法 #
tsx
export async function action({ request }: ActionFunctionArgs) {
switch (request.method) {
case "POST":
return handleCreate(request);
case "PUT":
return handleUpdate(request);
case "DELETE":
return handleDelete(request);
default:
return json({ error: "不支持的请求方法" }, { status: 405 });
}
}
async function handleCreate(request: Request) {
const formData = await request.formData();
const post = await createPost(formData);
return redirect(`/posts/${post.id}`);
}
async function handleUpdate(request: Request) {
const formData = await request.formData();
const post = await updatePost(formData);
return json({ post });
}
async function handleDelete(request: Request) {
const formData = await request.formData();
await deletePost(formData.get("id"));
return redirect("/posts");
}
四、返回响应 #
4.1 重定向 #
tsx
import { redirect } from "@remix-run/node";
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const post = await createPost(formData);
return redirect(`/posts/${post.id}`);
}
4.2 返回数据 #
tsx
import { json } from "@remix-run/node";
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const result = await createPost(formData);
return json({ success: true, post: result });
}
4.3 返回错误 #
tsx
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const title = formData.get("title");
if (!title || title.toString().length < 3) {
return json(
{ error: "标题至少需要3个字符" },
{ status: 400 }
);
}
const post = await createPost(formData);
return redirect(`/posts/${post.id}`);
}
五、使用Action数据 #
5.1 useActionData #
tsx
import { Form, useActionData } from "@remix-run/react";
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const errors = validateForm(formData);
if (errors) {
return json({ errors }, { status: 400 });
}
await createPost(formData);
return redirect("/posts");
}
export default function NewPost() {
const actionData = useActionData<typeof action>();
return (
<Form method="post">
<div>
<label>标题</label>
<input name="title" />
{actionData?.errors?.title && (
<p className="error">{actionData.errors.title}</p>
)}
</div>
<button type="submit">发布</button>
</Form>
);
}
5.2 useNavigation #
tsx
import { Form, useNavigation, useActionData } from "@remix-run/react";
export default function NewPost() {
const navigation = useNavigation();
const actionData = useActionData<typeof action>();
const isSubmitting = navigation.state === "submitting";
return (
<Form method="post">
<div>
<label>标题</label>
<input name="title" disabled={isSubmitting} />
{actionData?.errors?.title && (
<p className="error">{actionData.errors.title}</p>
)}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "发布中..." : "发布"}
</button>
</Form>
);
}
六、表单提交到其他路由 #
6.1 action属性 #
tsx
<Form method="post" action="/posts/new">
{/* 提交到 /posts/new 的 action */}
</Form>
6.2 相对路径 #
tsx
// 当前路径: /posts/123
<Form method="post" action="edit">
{/* 提交到 /posts/123/edit */}
</Form>
<Form method="post" action="../delete">
{/* 提交到 /posts/delete */}
</Form>
七、隐藏字段 #
7.1 传递额外数据 #
tsx
<Form method="post">
<input type="hidden" name="postId" value={post.id} />
<input type="hidden" name="action" value="publish" />
<button type="submit">发布</button>
</Form>
7.2 Intent按钮 #
tsx
<Form method="post">
<input name="title" />
<button type="submit" name="intent" value="save">
保存草稿
</button>
<button type="submit" name="intent" value="publish">
发布
</button>
</Form>
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const intent = formData.get("intent");
switch (intent) {
case "save":
return saveDraft(formData);
case "publish":
return publishPost(formData);
default:
return json({ error: "未知操作" }, { status: 400 });
}
}
八、文件上传 #
8.1 基本文件上传 #
tsx
export default function Upload() {
return (
<Form method="post" encType="multipart/form-data">
<input type="file" name="file" />
<button type="submit">上传</button>
</Form>
);
}
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const file = formData.get("file") as File;
if (!file) {
return json({ error: "请选择文件" }, { status: 400 });
}
const arrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
await saveFile(file.name, buffer);
return json({ success: true, filename: file.name });
}
8.2 多文件上传 #
tsx
export default function MultiUpload() {
return (
<Form method="post" encType="multipart/form-data">
<input type="file" name="files" multiple />
<button type="submit">上传</button>
</Form>
);
}
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const files = formData.getAll("files") as File[];
for (const file of files) {
await saveFile(file);
}
return json({ success: true, count: files.length });
}
九、JSON数据提交 #
9.1 使用useFetcher #
tsx
import { useFetcher } from "@remix-run/react";
export default function Component() {
const fetcher = useFetcher();
const handleSubmit = async (data: any) => {
fetcher.submit(data, {
method: "post",
encType: "application/json",
});
};
return (
<button onClick={() => handleSubmit({ name: "test" })}>
提交JSON
</button>
);
}
export async function action({ request }: ActionFunctionArgs) {
const data = await request.json();
return json({ received: data });
}
十、最佳实践 #
10.1 表单验证 #
tsx
import { z } from "zod";
const postSchema = z.object({
title: z.string().min(3, "标题至少3个字符"),
content: z.string().min(10, "内容至少10个字符"),
});
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const result = postSchema.safeParse(Object.fromEntries(formData));
if (!result.success) {
return json(
{ errors: result.error.flatten().fieldErrors },
{ status: 400 }
);
}
const post = await createPost(result.data);
return redirect(`/posts/${post.id}`);
}
10.2 防止重复提交 #
tsx
export default function Form() {
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
return (
<Form method="post">
{/* 表单字段 */}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "提交中..." : "提交"}
</button>
</Form>
);
}
10.3 成功提示 #
tsx
import { useActionData, useNavigation } from "@remix-run/react";
export default function Form() {
const actionData = useActionData<typeof action>();
const navigation = useNavigation();
return (
<div>
{actionData?.success && (
<div className="success">操作成功!</div>
)}
<Form method="post">
{/* 表单字段 */}
</Form>
</div>
);
}
十一、总结 #
本章我们学习了:
- Action定义:使用 action 函数处理数据变更
- 表单提交:Form 组件和方法设置
- 数据获取:formData 和表单数据解析
- 响应处理:重定向和返回数据
- 文件上传:处理文件上传请求
核心要点:
- Action 在服务端执行,处理数据修改
- 使用 Form 组件增强表单功能
- 使用 useActionData 获取 action 返回的数据
- 使用 useNavigation 获取提交状态
最后更新:2026-03-28