TL;DR

フォームからサーバの非同期関数を直接呼び出せる React の機能。Next.js ではなく React 本体の機能である。

なぜ必要か

従来は、API クライアントを通して Route Handler を呼び出していた。Server Actions を使うと、次の利点がある。

  • 中間のコード(API クライアント)がなくなる
  • ブラウザ向けにバンドルされる API クライアントのコードが減る
  • ハイドレーションが終わる前にフォームを送信できる

最後の点が大きい。従来は onSubmit イベントハンドラで送信していたので、ハイドレーションが終わるまでユーザはフォームを送信できなかった。Server Actions は <form> の action 属性を使うので、ハイドレーション前でも送信でき、Progressive Enhancement が実現できる。

仕組み

use server ディレクティブ

Server Action を使うには "use server" ディレクティブを宣言する。このディレクティブは、React Server Component などサーバサイドのファイルでのみ有効である。宣言する場所は 2 通りある。

1 つ目は、非同期関数の本体の先頭に書く方法である。

export default function ServerComponent() {
  async function myAction(formData: FormData) {
    "use server"
    // ...
  }
}

2 つ目は、Server Action 専用のファイルを作り、その先頭に書く方法である。ファイル名は自由で、定義した関数は Server Component からも Client Component からも import して使える。

// actions.ts
"use server"
 
export async function myAction(formData: FormData) {
  // ...
}

Server Action の関数は、第 1 引数に Web 標準の FormData オブジェクトを受け取り、そこからフォームの入力値を参照する。

引数のバインド

hidden フィールドの代わりに、bind メソッドで引数を渡すこともできる。bind は、いくつかの引数を固定した新しい Server Action を作る。このとき関数は、固定した引数の後ろに FormData を受け取る。

"use client"
 
import { myAction } from "./actions"
 
export default function ClientComponent({ id }: { id: string }) {
  const action = myAction.bind(null, id)
 
  return (
    <form action={action}>
      <button type="submit">Add to Cart</button>
    </form>
  )
}
export async function myAction(id: string, formData: FormData) {
  // ...
}

例

Progressive Enhancement を保ったままクライアント側でバリデーションするには、ハイドレーション前の送信ではクライアントのバリデーションを行わず、ハイドレーション後の送信では行えばよい。これは <form> の action と onSubmit を分けると実現できる。onSubmit でバリデーションに失敗したら preventDefault を呼び、action の実行を止める。

export function PhotoForm({ photo, categories }: Props) {
  const [formState, formDispatch] = useFormState(updatePhoto, initialFormState({ ...photo }))
  const [clientErrors, setClientErrors] = useState<FieldErrors | undefined>(
    formState.error?.fieldErrors,
  )
  const errors = clientErrors || formState.error?.fieldErrors
 
  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    try {
      const formData = new FormData(event.currentTarget)
      validateFormData(formData)
      setClientErrors(undefined)
    } catch (err) {
      // クライアントのバリデーションに失敗したら action の実行を止める
      event.preventDefault()
      if (!(err instanceof ZodError)) throw err
      setClientErrors(transformFiledErrors(err))
    }
  }
 
  return (
    <form action={formDispatch} onSubmit={handleSubmit}>
      {/* ... */}
    </form>
  )
}

自分の理解

SvelteKit の Form actions とほとんど同じイメージである。

関連

出典