Skip to content

Server Actions

Server Actions 是在服务端执行的异步函数,可以在服务端组件和客户端组件中使用,处理 nextjs 应用中的数据和更改

模式

模块级别

tsx
// app/actions.tsx
"use server";

const data = ["吃饭", "睡觉", "coding"];

async function getTodos() {
  return data;
}
  • 可以在服务端/客户端组件使用

函数级别

tsx
// app/page.tsx
export default function Page() {
  async function getTodos() {
    "use server";
    const data = ["吃饭", "睡觉", "coding"];
    return data;
  }
}
  • 只能在服务端组件只使用

应用场景

  • 在 page router 中采用传统接口调用模式时,需要先定义接口,再由客户端调用并完成数据交互
  • 在 app router 中,这种操作可以直接简化为 server actions

Page Router

ts
// app/api/todo-list/route.ts
import { NextRequest, NextResponse } from "next/server";

const data = ["吃饭", "冥想", "打豆豆"];

export function GET() {
  return NextResponse.json({ data });
}

export async function POST(request: NextRequest) {
  const formData = await request.formData();
  const result = formData.get("key") as string;
  data.push(result);
  return NextResponse.json({ data });
}
tsx
// pages/form.tsx

"use client";

import { FormEvent, useEffect, useState } from "react";

function Form() {
  const [todos, setTodos] = useState([]);

  useEffect(() => {
    async function getTodos() {
      const res = await fetch("/api/todo-list");
      const data = await res.json();
      setTodos(data.data);
    }
    getTodos();
  }, []);

  const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    const res = await fetch("/api/todo-list", {
      method: "POST",
      body: new FormData(e.target as HTMLFormElement),
    });
    const data = await res.json();
    setTodos(data.data);
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input type="text" name="key" />
        <button type="submit">Add</button>
      </form>
      <h3>Todo List</h3>
      <ul>
        {todos.map((todo) => (
          <li key={todo}>{todo}</li>
        ))}
      </ul>
    </div>
  );
}

export default Form;

App Router

tsx
// app/form/actions.ts
"use server";

import { revalidatePath } from "next/cache";

const data = ["吃饭", "睡觉", "coding"];

async function getTodos() {
  return data;
}

async function addTodos(formData) {
  const todo = formData.get("todo");
  data.push(todo);
  revalidatePath("/form");
  return data;
}
tsx
// app/form/page.tsx
import { getTodos, addTodos } from "./actions";

async function Page() {
  const todos = await getTodos();

  return (
    <>
      <form action={addTodos}>
        <input name="todos" />
        <button type="submit">add</button>
      </form>
      <ul>
        {todos.map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </>
  );
}

export default Page;

原理

当 form action 触发,实际是向当前页面发送了一个 post 请求,form 标签中注入了一个 hidden 的 input,带有一个 action_id 用于区分是哪个 form 的请求

html
<form>
  <input
    type="hidden"
    name="$ACTION_ID_7f8a5a30e5f9c77460d0fb2c8e233829af998c237b"
  />
</form>

实战案例

tsx
// actions.ts
"use server";

import { revalidatePath } from "next/cache";
const todos = ["吃饭", "睡觉", "coding"];

export async function getTodos() {
  return todos;
}

export async function createTodo(_, formData: FormData) {
  const todo = formData.get("todo") as string;
  await new Promise((resolve) => setTimeout(resolve, 1000));
  todos.push(todo);
  revalidatePath("/form-actions-3");
  return {
    message: `add ${todo} success`,
  };
}
tsx
// form/form.tsx
"use client";

import { useActionState } from "react";
import { createTodo } from "./actions";

const FormActions = () => {
  const [state, formAction, isPending] = useActionState(createTodo, {
    message: "",
  });

  return (
    <form action={formAction}>
      <input type="text" name="todo" />
      <button type="submit">{isPending ? "Adding..." : "Add"}</button>
      <p>{state.message}</p>
    </form>
  );
};

export default FormActions3;
tsx
import { getTodos } from "./actions";
import FormActions from "./form";

const FormActions3Page = async () => {
  const todos = await getTodos();

  return (
    <div>
      <FormActions />
      <ul>
        {todos.map((todo) => (
          <li key={todo}>{todo}</li>
        ))}
      </ul>
    </div>
  );
};

export default FormActions3Page;