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>
  );
}

十一、总结 #

本章我们学习了:

  1. Action定义:使用 action 函数处理数据变更
  2. 表单提交:Form 组件和方法设置
  3. 数据获取:formData 和表单数据解析
  4. 响应处理:重定向和返回数据
  5. 文件上传:处理文件上传请求

核心要点:

  • Action 在服务端执行,处理数据修改
  • 使用 Form 组件增强表单功能
  • 使用 useActionData 获取 action 返回的数据
  • 使用 useNavigation 获取提交状态
最后更新:2026-03-28