数据修改与 Server Actions

Next.js 使用 React 的 Server Functions 来处理写入。带有 use server 指令的异步函数可以被客户端通过网络调用。当它和 <form>formAction 搭配时,就是大家常说的 Server Action

创建 Server Function

在文件级别放 'use server',导出的函数都会变成 Server Function:

// app/actions.ts
'use server';

import { auth } from '@/lib/auth';

export async function createPost(formData: FormData) {
  const session = await auth();
  if (!session?.user) throw new Error('Unauthorized');

  const title = formData.get('title');
  const content = formData.get('content');
  // 写数据库…
}

安全警告:Server Function 会暴露成一个 POST 端点,必须在函数内自行做鉴权与授权,不能只依赖 UI 层的按钮显示与否。

也可以在函数顶部写 'use server',仅标记单个函数:

export default function Page() {
  async function createPost(formData: FormData) {
    'use server';
    // …
  }
  return <form action={createPost}>…</form>;
}

在表单中调用

React 扩展了 <form>action prop,可以直接传 Server Function:

import { createPost } from '@/app/actions';

export function Form() {
  return (
    <form action={createPost}>
      <input name="title" />
      <textarea name="content" />
      <button type="submit">发布</button>
    </form>
  );
}
  • 即便 JS 还没加载,表单也能提交(渐进增强)。
  • 提交后,Next.js 会在同一次请求里返回更新后的 UI。

在事件里调用

客户端组件里用 onClick 等事件调用:

'use client';
import { incrementLike } from './actions';
import { useState } from 'react';

export default function LikeButton({ initialLikes }: { initialLikes: number }) {
  const [likes, setLikes] = useState(initialLikes);
  return (
    <button onClick={async () => setLikes(await incrementLike())}>
      ❤ {likes}
    </button>
  );
}

处理期望内的错误

对于表单校验这类「可预期错误」,不要 throw,直接把错误当 返回值 返回:

'use server';
export async function createPost(prevState: any, formData: FormData) {
  const res = await fetch('https://api.vercel.app/posts', {
    method: 'POST',
    body: JSON.stringify({
      title: formData.get('title'),
      content: formData.get('content'),
    }),
  });
  if (!res.ok) return { message: '创建失败' };
  return { message: '' };
}

在客户端用 useActionState 管理:

'use client';
import { useActionState } from 'react';
import { createPost } from '@/app/actions';

export function Form() {
  const [state, formAction, pending] = useActionState(createPost, { message: '' });
  return (
    <form action={formAction}>
      <input name="title" required />
      <textarea name="content" required />
      {state?.message && <p aria-live="polite">{state.message}</p>}
      <button disabled={pending}>发布</button>
    </form>
  );
}

写入后刷新 UI

提交完通常需要刷新数据。按需选择:

'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
import { redirect } from 'next/navigation';

export async function createPost(formData: FormData) {
  // 写数据…
  revalidatePath('/posts');   // 按路径失效
  // revalidateTag('posts'); // 按 tag 失效
  redirect('/posts');         // 重定向
}
  • revalidatePath / revalidateTag:让缓存失效,下次访问拉最新数据。
  • refresh()(来自 next/cache):刷新客户端路由,但不会让 tag 缓存失效。
  • redirect():跳转到新页面,后续代码不会执行。

操作 Cookie

'use server';
import { cookies } from 'next/headers';

export async function signIn() {
  const store = await cookies();
  store.set('token', 'xxx');    // 设置
  store.get('token')?.value;    // 读取
  store.delete('token');         // 删除
}

Next.js 在 Server Action 设置 cookie 后会自动重新渲染当前页与布局,让 UI 跟上。

資料修改與 Server Actions

Next.js 使用 React 的 Server Functions 處理寫入。帶有 use server 指令的非同步函式,可被客戶端透過網路呼叫;與 <form>formAction 搭配時,就是常說的 Server Action

建立 Server Function

在檔案層級放 'use server',所有匯出函式都會變成 Server Function:

// app/actions.ts
'use server';

import { auth } from '@/lib/auth';

export async function createPost(formData: FormData) {
  const session = await auth();
  if (!session?.user) throw new Error('Unauthorized');

  const title = formData.get('title');
  const content = formData.get('content');
  // 寫入資料庫…
}

安全警告:Server Function 會被暴露為一個 POST 端點,必須在函式內自行做驗證與授權,不能只靠 UI 層的按鈕是否顯示。

也可以在函式頂部寫 'use server',僅標記單一函式:

export default function Page() {
  async function createPost(formData: FormData) {
    'use server';
    // …
  }
  return <form action={createPost}>…</form>;
}

在表單中呼叫

React 擴展了 <form>action prop,可直接傳 Server Function:

import { createPost } from '@/app/actions';

export function Form() {
  return (
    <form action={createPost}>
      <input name="title" />
      <textarea name="content" />
      <button type="submit">發佈</button>
    </form>
  );
}
  • 即使 JS 尚未載入,表單仍能提交(漸進增強)。
  • 送出後,Next.js 會在同一次請求返回更新後的 UI。

在事件中呼叫

客戶端元件中可用 onClick 等事件呼叫:

'use client';
import { incrementLike } from './actions';
import { useState } from 'react';

export default function LikeButton({ initialLikes }: { initialLikes: number }) {
  const [likes, setLikes] = useState(initialLikes);
  return (
    <button onClick={async () => setLikes(await incrementLike())}>
      ❤ {likes}
    </button>
  );
}

處理預期內的錯誤

對於表單驗證這類「可預期錯誤」,請勿 throw,直接把錯誤當 回傳值 返回:

'use server';
export async function createPost(prevState: any, formData: FormData) {
  const res = await fetch('https://api.vercel.app/posts', {
    method: 'POST',
    body: JSON.stringify({
      title: formData.get('title'),
      content: formData.get('content'),
    }),
  });
  if (!res.ok) return { message: '建立失敗' };
  return { message: '' };
}

在客戶端以 useActionState 管理:

'use client';
import { useActionState } from 'react';
import { createPost } from '@/app/actions';

export function Form() {
  const [state, formAction, pending] = useActionState(createPost, { message: '' });
  return (
    <form action={formAction}>
      <input name="title" required />
      <textarea name="content" required />
      {state?.message && <p aria-live="polite">{state.message}</p>}
      <button disabled={pending}>發佈</button>
    </form>
  );
}

寫入後刷新 UI

提交後通常需要刷新資料。按需選擇:

'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
import { redirect } from 'next/navigation';

export async function createPost(formData: FormData) {
  // 寫入資料…
  revalidatePath('/posts');   // 依路徑失效
  // revalidateTag('posts'); // 依 tag 失效
  redirect('/posts');         // 重新導向
}
  • revalidatePath / revalidateTag:令快取失效,下次存取拉取最新資料。
  • refresh()(來自 next/cache):刷新客戶端路由,但不會令 tag 快取失效。
  • redirect():跳轉到新頁面,其後程式碼不會執行。

操作 Cookie

'use server';
import { cookies } from 'next/headers';

export async function signIn() {
  const store = await cookies();
  store.set('token', 'xxx');    // 設定
  store.get('token')?.value;    // 讀取
  store.delete('token');         // 刪除
}

Next.js 在 Server Action 設定 cookie 後會自動重新渲染目前頁與佈局,讓 UI 同步。

Mutating Data with Server Actions

Next.js handles writes through React's Server Functions — async functions marked with 'use server' that the client can invoke over the network. When wired to a <form> or formAction, we call them Server Actions.

Creating a Server Function

Put 'use server' at the top of a file to mark every export as a Server Function:

// app/actions.ts
'use server';

import { auth } from '@/lib/auth';

export async function createPost(formData: FormData) {
  const session = await auth();
  if (!session?.user) throw new Error('Unauthorized');

  const title = formData.get('title');
  const content = formData.get('content');
  // write to the database…
}

Security warning: Server Functions are exposed as POST endpoints. Authenticate and authorize inside every function. Don't rely on whether a button is rendered.

You can also mark a single function with 'use server':

export default function Page() {
  async function createPost(formData: FormData) {
    'use server';
    // …
  }
  return <form action={createPost}>…</form>;
}

Invoking from a form

React extends <form>'s action prop to accept Server Functions:

import { createPost } from '@/app/actions';

export function Form() {
  return (
    <form action={createPost}>
      <input name="title" />
      <textarea name="content" />
      <button type="submit">Publish</button>
    </form>
  );
}
  • Forms submit even before JavaScript has loaded (progressive enhancement).
  • After submit, Next.js returns the updated UI in the same round-trip.

Invoking from an event handler

Client Components can call actions from handlers:

'use client';
import { incrementLike } from './actions';
import { useState } from 'react';

export default function LikeButton({ initialLikes }: { initialLikes: number }) {
  const [likes, setLikes] = useState(initialLikes);
  return (
    <button onClick={async () => setLikes(await incrementLike())}>
      ❤ {likes}
    </button>
  );
}

Handling expected errors

For validation-style "expected errors", don't throw — return them as values:

'use server';
export async function createPost(prevState: any, formData: FormData) {
  const res = await fetch('https://api.vercel.app/posts', {
    method: 'POST',
    body: JSON.stringify({
      title: formData.get('title'),
      content: formData.get('content'),
    }),
  });
  if (!res.ok) return { message: 'Failed to create post' };
  return { message: '' };
}

On the client, manage it with useActionState:

'use client';
import { useActionState } from 'react';
import { createPost } from '@/app/actions';

export function Form() {
  const [state, formAction, pending] = useActionState(createPost, { message: '' });
  return (
    <form action={formAction}>
      <input name="title" required />
      <textarea name="content" required />
      {state?.message && <p aria-live="polite">{state.message}</p>}
      <button disabled={pending}>Publish</button>
    </form>
  );
}

Refreshing UI after a mutation

After a write you usually need to refresh data. Pick the right tool:

'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
import { redirect } from 'next/navigation';

export async function createPost(formData: FormData) {
  // mutate…
  revalidatePath('/posts');   // invalidate by path
  // revalidateTag('posts'); // invalidate by tag
  redirect('/posts');         // navigate away
}
  • revalidatePath / revalidateTag: invalidate the cache so the next read is fresh.
  • refresh() (from next/cache): refreshes the client router — does not revalidate tag caches.
  • redirect(): throws a framework-handled signal and navigates; nothing after runs.

Cookies

'use server';
import { cookies } from 'next/headers';

export async function signIn() {
  const store = await cookies();
  store.set('token', 'xxx');    // set
  store.get('token')?.value;    // read
  store.delete('token');         // delete
}

After a Server Action sets or deletes a cookie, Next.js re-renders the current page and its layouts so the UI reflects the new value.