サーバーアクションとデータ変更 (Server Actions and Mutations)

サーバーアクション (Server Actions) は、サーバー上で実行される非同期関数です。Next.jsアプリケーションでフォーム送信やデータ変更を処理するために、サーバーコンポーネントとクライアントコンポーネントの両方で使用できます。

🎥 動画で学ぶ: サーバーアクションを使ったフォームとデータ変更について → YouTube (10分)

規約

サーバーアクションはReactの"use server"ディレクティブで定義できます。このディレクティブをasync関数の先頭に追加して関数をサーバーアクションとしてマークするか、別ファイルの先頭に追加してそのファイルのすべてのエクスポートをサーバーアクションとしてマークできます。

サーバーコンポーネント

サーバーコンポーネントでは、インライン関数レベルまたはモジュールレベルの"use server"ディレクティブを使用できます。サーバーアクションをインラインで定義するには、関数本体の先頭に"use server"を追加します:

// サーバーコンポーネント
export default function Page() {
  // サーバーアクション
  async function create() {
    'use server'

    // ...
  }

  return (
    // ...
  )
}
// サーバーコンポーネント
export default function Page() {
  // サーバーアクション
  async function create() {
    'use server'

    // ...
  }

  return (
    // ...
  )
}

クライアントコンポーネント

クライアントコンポーネントでは、モジュールレベルの"use server"ディレクティブを使用したアクションのみをインポートできます。

クライアントコンポーネントでサーバーアクションを呼び出すには、新しいファイルを作成し、その先頭に"use server"ディレクティブを追加します。ファイル内のすべての関数は、クライアントとサーバーコンポーネントの両方で再利用可能なサーバーアクションとしてマークされます:

'use server'

export async function create() {
  // ...
}
'use server'

export async function create() {
  // ...
}
import { create } from '@/app/actions'

export function Button() {
  return (
    // ...
  )
}
import { create } from '@/app/actions'

export function Button() {
  return (
    // ...
  )
}

また、サーバーアクションをプロップとしてクライアントコンポーネントに渡すこともできます:

<ClientComponent updateItem={updateItem} />
app/client-component.jsx
'use client'

export default function ClientComponent({ updateItem }) {
  return <form action={updateItem}>{/* ... */}</form>
}

動作特性

  • サーバーアクションは<form>要素action属性を使用して呼び出せます:
    • サーバーコンポーネントはデフォルトでプログレッシブエンハンスメントをサポートしており、JavaScriptがまだロードされていないか無効になっていてもフォームが送信されます。
    • クライアントコンポーネントでは、JavaScriptがロードされていない場合、サーバーアクションを呼び出すフォームの送信はキューに入れられ、クライアントのハイドレーションが優先されます。
    • ハイドレーション後、フォーム送信時にブラウザはリフレッシュされません。
  • サーバーアクションは<form>に限定されず、イベントハンドラー、useEffect、サードパーティライブラリ、および<button>などの他のフォーム要素から呼び出せます。
  • サーバーアクションはNext.jsのキャッシュと再検証アーキテクチャと統合されています。アクションが呼び出されると、Next.jsは更新されたUIと新しいデータを単一のサーバーラウンドトリップで返すことができます。
  • 内部的に、アクションはPOSTメソッドを使用し、このHTTPメソッドのみがアクションを呼び出せます。
  • サーバーアクションの引数と戻り値はReactによってシリアライズ可能でなければなりません。シリアライズ可能な引数と値のリストについてはReactのドキュメントを参照してください。
  • サーバーアクションは関数です。つまり、アプリケーションのどこでも再利用できます。
  • サーバーアクションは、使用されるページまたはレイアウトからランタイムを継承します。
  • サーバーアクションは、使用されるページまたはレイアウトからルートセグメント設定を継承し、maxDurationなどのフィールドを含みます。

フォーム

ReactはHTMLの<form>要素を拡張し、actionプロップでサーバーアクションを呼び出せるようにしています。

フォームで呼び出されると、アクションは自動的にFormDataオブジェクトを受け取ります。ReactのuseStateを使用してフィールドを管理する必要はなく、代わりにネイティブのFormDataメソッドを使用してデータを抽出できます:

export default function Page() {
  async function createInvoice(formData: FormData) {
    'use server'

    const rawFormData = {
      customerId: formData.get('customerId'),
      amount: formData.get('amount'),
      status: formData.get('status'),
    }

    // データを変更
    // キャッシュを再検証
  }

  return <form action={createInvoice}>...</form>
}
export default function Page() {
  async function createInvoice(formData) {
    'use server'

    const rawFormData = {
      customerId: formData.get('customerId'),
      amount: formData.get('amount'),
      status: formData.get('status'),
    }

    // データを変更
    // キャッシュを再検証
  }

  return <form action={createInvoice}>...</form>
}

知っておくと便利:

追加の引数を渡す

JavaScriptのbindメソッドを使用して、サーバーアクションに追加の引数を渡せます。

'use client'

import { updateUser } from './actions'

export function UserProfile({ userId }: { userId: string }) {
  const updateUserWithId = updateUser.bind(null, userId)

  return (
    <form action={updateUserWithId}>
      <input type="text" name="name" />
      <button type="submit">ユーザー名を更新</button>
    </form>
  )
}
'use client'

import { updateUser } from './actions'

export function UserProfile({ userId }) {
  const updateUserWithId = updateUser.bind(null, userId)

  return (
    <form action={updateUserWithId}>
      <input type="text" name="name" />
      <button type="submit">ユーザー名を更新</button>
    </form>
  )
}

サーバーアクションは、フォームデータに加えてuserId引数を受け取ります:

app/actions.js
'use server'

export async function updateUser(userId, formData) {
  // ...
}

知っておくと便利:

  • 別の方法として、フォーム内に隠し入力フィールドとして引数を渡すこともできます(例: <input type="hidden" name="userId" value={userId} />)。ただし、値はレンダリングされたHTMLの一部となり、エンコードされません。
  • .bindはサーバーとクライアントコンポーネントの両方で動作します。また、プログレッシブエンハンスメントもサポートしています。

保留状態

ReactのuseFormStatusフックを使用して、フォームが送信されている間の保留状態を表示できます。

  • useFormStatusは特定の<form>のステータスを返すため、<form>要素の子として定義する必要があります
  • useFormStatusはReactフックであるため、クライアントコンポーネントで使用する必要があります。
'use client'

import { useFormStatus } from 'react-dom'

export function SubmitButton() {
  const { pending } = useFormStatus()

  return (
    <button type="submit" disabled={pending}>
      追加
    </button>
  )
}
'use client'

import { useFormStatus } from 'react-dom'

export function SubmitButton() {
  const { pending } = useFormStatus()

  return (
    <button type="submit" disabled={pending}>
      追加
    </button>
  )
}

<SubmitButton />は任意のフォーム内にネストできます:

import { SubmitButton } from '@/app/submit-button'
import { createItem } from '@/app/actions'

// サーバーコンポーネント
export default async function Home() {
  return (
    <form action={createItem}>
      <input type="text" name="field-name" />
      <SubmitButton />
    </form>
  )
}
import { SubmitButton } from '@/app/submit-button'
import { createItem } from '@/app/actions'

// サーバーコンポーネント
export default async function Home() {
  return (
    <form action={createItem}>
      <input type="text" name="field-name" />
      <SubmitButton />
    </form>
  )
}

サーバーサイド検証とエラーハンドリング

基本的なクライアントサイドフォーム検証には、requiredtype="email"などのHTML検証を使用することを推奨します。

より高度なサーバーサイド検証には、zodなどのライブラリを使用して、データを変更する前にフォームフィールドを検証できます:

'use server'

import { z } from 'zod'

const schema = z.object({
  email: z.string({
    invalid_type_error: '無効なメールアドレス',
  }),
})

export default async function createUser(formData: FormData) {
  const validatedFields = schema.safeParse({
    email: formData.get('email'),
  })

  // フォームデータが無効な場合は早期リターン
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    }
  }

  // データを変更
}
'use server'

import { z } from 'zod'

const schema = z.object({
  email: z.string({
    invalid_type_error: '無効なメールアドレス',
  }),
})

export default async function createsUser(formData) {
  const validatedFields = schema.safeParse({
    email: formData.get('email'),
  })

  // フォームデータが無効な場合は早期リターン
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    }
  }

  // データを変更
}

サーバーでフィールドが検証されたら、アクション内でシリアライズ可能なオブジェクトを返し、ReactのuseFormStateフックを使用してユーザーにメッセージを表示できます。

  • アクションをuseFormStateに渡すと、アクションの関数シグネチャが変更され、最初の引数として新しいprevStateまたはinitialStateパラメータを受け取ります。
  • useFormStateはReactフックであるため、クライアントコンポーネントで使用する必要があります。
'use server'

export async function createUser(prevState: any, formData: FormData) {
  // ...
  return {
    message: '有効なメールアドレスを入力してください',
  }
}
'use server'

export async function createUser(prevState, formData) {
  // ...
  return {
    message: '有効なメールアドレスを入力してください',
  }
}

次に、アクションをuseFormStateフックに渡し、返されたstateを使用してエラーメッセージを表示できます。

'use client'

import { useFormState } from 'react-dom'
import { createUser } from '@/app/actions'

const initialState = {
  message: '',
}

export function Signup() {
  const [state, formAction] = useFormState(createUser, initialState)

  return (
    <form action={formAction}>
      <label htmlFor="email">メールアドレス</label>
      <input type="text" id="email" name="email" required />
      {/* ... */}
      <p aria-live="polite" className="sr-only">
        {state?.message}
      </p>
      <button>サインアップ</button>
    </form>
  )
}
'use client'

import { useFormState } from 'react-dom'
import { createUser } from '@/app/actions'

const initialState = {
  message: '',
}

export function Signup() {
  const [state, formAction] = useFormState(createUser, initialState)

  return (
    <form action={formAction}>
      <label htmlFor="email">メールアドレス</label>
      <input type="text" id="email" name="email" required />
      {/* ... */}
      <p aria-live="polite" className="sr-only">
        {state?.message}
      </p>
      <button>サインアップ</button>
    </form>
  )
}

知っておくと便利:

  • データを変更する前に、ユーザーがそのアクションを実行する権限があることを常に確認してください。認証と認可を参照してください。

楽観的更新

ReactのuseOptimisticフックを使用して、サーバーアクションが完了するのを待たずに、UIを楽観的に更新できます:

'use client'

import { useOptimistic } from 'react'
import { send } from './actions'

type Message = {
  message: string
}

export function Thread({ messages }: { messages: Message[] }) {
  const [optimisticMessages, addOptimisticMessage] = useOptimistic<
    Message[],
    string
  >(messages, (state, newMessage) => [...state, { message: newMessage }])

  return (
    <div>
      {optimisticMessages.map((m, k) => (
        <div key={k}>{m.message}</div>
      ))}
      <form
        action={async (formData: FormData) => {
          const message = formData.get('message')
          addOptimisticMessage(message)
          await send(message)
        }}
      >
        <input type="text" name="message" />
        <button type="submit">送信</button>
      </form>
    </div>
  )
}
'use client'

import { useOptimistic } from 'react'
import { send } from './actions'

export function Thread({ messages }) {
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) => [...state, { message: newMessage }]
  )

  return (
    <div>
      {optimisticMessages.map((m) => (
        <div>{m.message}</div>
      ))}
      <form
        action={async (formData) => {
          const message = formData.get('message')
          addOptimisticMessage(message)
          await send(message)
        }}
      >
        <input type="text" name="message" />
        <button type="submit">送信</button>
      </form>
    </div>
  )
}

ネストされた要素

<button><input type="submit"><input type="image">など、<form>内にネストされた要素でサーバーアクションを呼び出せます。これらの要素はformActionプロップまたはイベントハンドラーを受け入れます。

これは、フォーム内で複数のサーバーアクションを呼び出したい場合に便利です。例えば、投稿を公開するだけでなく、下書きとして保存するための特定の<button>要素を作成できます。詳細はReactの<form>ドキュメントを参照してください。

プログラムによるフォーム送信

requestSubmit()メソッドを使用してフォーム送信をトリガーできます。例えば、ユーザーが + Enterを押したときにonKeyDownイベントをリッスンできます:

'use client'

export function Entry() {
  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
    if (
      (e.ctrlKey || e.metaKey) &&
      (e.key === 'Enter' || e.key === 'NumpadEnter')
    ) {
      e.preventDefault()
      e.currentTarget.form?.requestSubmit()
    }
  }

  return (
    <div>
      <textarea name="entry" rows={20} required onKeyDown={handleKeyDown} />
    </div>
  )
}
'use client'

export function Entry() {
  const handleKeyDown = (e) => {
    if (
      (e.ctrlKey || e.metaKey) &&
      (e.key === 'Enter' || e.key === 'NumpadEnter')
    ) {
      e.preventDefault()
      e.currentTarget.form?.requestSubmit()
    }
  }

  return (
    <div>
      <textarea name="entry" rows={20} required onKeyDown={handleKeyDown} />
    </div>
  )
}

これにより、最も近い<form>祖先要素の送信がトリガーされ、サーバーアクションが呼び出されます。

フォーム以外の要素

サーバーアクションは<form>要素内で使用するのが一般的ですが、イベントハンドラやuseEffectなど、コードの他の部分からも呼び出すことができます。

イベントハンドラ

onClickなどのイベントハンドラからサーバーアクションを呼び出せます。例えば、いいね数を増やす場合:

'use client'

import { incrementLike } from './actions'
import { useState } from 'react'

export default function LikeButton({ initialLikes }: { initialLikes: number }) {
  const [likes, setLikes] = useState(initialLikes)

  return (
    <>
      <p>Total Likes: {likes}</p>
      <button
        onClick={async () => {
          const updatedLikes = await incrementLike()
          setLikes(updatedLikes)
        }}
      >
        Like
      </button>
    </>
  )
}
'use client'

import { incrementLike } from './actions'
import { useState } from 'react'

export default function LikeButton({ initialLikes }) {
  const [likes, setLikes] = useState(initialLikes)

  return (
    <>
      <p>Total Likes: {likes}</p>
      <button
        onClick={async () => {
          const updatedLikes = await incrementLike()
          setLikes(updatedLikes)
        }}
      >
        Like
      </button>
    </>
  )
}

ユーザーエクスペリエンスを向上させるため、useOptimisticuseTransitionなどのReact APIを使用して、サーバーアクションの実行完了前にUIを更新したり、保留状態を表示することを推奨します。

フォーム要素にもイベントハンドラを追加できます。例えば、フォームフィールドをonChangeで保存する場合:

app/ui/edit-post.tsx
'use client'

import { publishPost, saveDraft } from './actions'

export default function EditPost() {
  return (
    <form action={publishPost}>
      <textarea
        name="content"
        onChange={async (e) => {
          await saveDraft(e.target.value)
        }}
      />
      <button type="submit">Publish</button>
    </form>
  )
}

このような場合、複数のイベントが短時間に発生する可能性があるため、不要なサーバーアクションの呼び出しを防ぐためにデバウンスすることを推奨します。

useEffect

ReactのuseEffectフックを使用して、コンポーネントのマウント時や依存関係の変更時にサーバーアクションを呼び出せます。これは、グローバルイベントに依存する変更や自動的にトリガーする必要がある場合に便利です。例えば、アプリショートカットのonKeyDown、無限スクロールの交差監視フック、またはコンポーネントマウント時のビュー数更新:

'use client'

import { incrementViews } from './actions'
import { useState, useEffect } from 'react'

export default function ViewCount({ initialViews }: { initialViews: number }) {
  const [views, setViews] = useState(initialViews)

  useEffect(() => {
    const updateViews = async () => {
      const updatedViews = await incrementViews()
      setViews(updatedViews)
    }

    updateViews()
  }, [])

  return <p>Total Views: {views}</p>
}
'use client'

import { incrementViews } from './actions'
import { useState, useEffect } from 'react'

export default function ViewCount({ initialViews }: { initialViews: number }) {
  const [views, setViews] = useState(initialViews)

  useEffect(() => {
    const updateViews = async () => {
      const updatedViews = await incrementViews()
      setViews(updatedViews)
    }

    updateViews()
  }, [])

  return <p>Total Views: {views}</p>
}

useEffectの動作と注意点を考慮してください。

エラーハンドリング

エラーがスローされると、クライアント側で最も近いerror.jsまたは<Suspense>境界でキャッチされます。UIで処理するためにtry/catchを使用してエラーを返すことを推奨します。

例えば、サーバーアクションは新しいアイテム作成時のエラーを処理してメッセージを返す場合:

'use server'

export async function createTodo(prevState: any, formData: FormData) {
  try {
    // データを変更
  } catch (e) {
    throw new Error('タスクの作成に失敗しました')
  }
}
'use server'

export async function createTodo(prevState, formData) {
  try {
    // データを変更
  } catch (e) {
    throw new Error('タスクの作成に失敗しました')
  }
}

知っておくと良いこと:

データの再検証

revalidatePath APIを使用して、サーバーアクション内でNext.jsキャッシュを再検証できます:

'use server'

import { revalidatePath } from 'next/cache'

export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }

  revalidatePath('/posts')
}
'use server'

import { revalidatePath } from 'next/cache'

export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }

  revalidatePath('/posts')
}

または、キャッシュタグを使用して特定のデータフェッチを無効化するにはrevalidateTagを使用します:

'use server'

import { revalidateTag } from 'next/cache'

export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }

  revalidateTag('posts')
}
'use server'

import { revalidateTag } from 'next/cache'

export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }

  revalidateTag('posts')
}

リダイレクト

サーバーアクションの完了後にユーザーを別のルートにリダイレクトしたい場合、redirect APIを使用できます。redirecttry/catchブロックの外で呼び出す必要があります:

'use server'

import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'

export async function createPost(id: string) {
  try {
    // ...
  } catch (error) {
    // ...
  }

  revalidateTag('posts') // キャッシュされた投稿を更新
  redirect(`/post/${id}`) // 新しい投稿ページに移動
}
'use server'

import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'

export async function createPost(id) {
  try {
    // ...
  } catch (error) {
    // ...
  }

  revalidateTag('posts') // キャッシュされた投稿を更新
  redirect(`/post/${id}`) // 新しい投稿ページに移動
}

cookies APIを使用して、サーバーアクション内でCookieを取得設定削除できます:

'use server'

import { cookies } from 'next/headers'

export async function exampleAction() {
  // Cookieを取得
  const value = cookies().get('name')?.value

  // Cookieを設定
  cookies().set('name', 'Delba')

  // Cookieを削除
  cookies().delete('name')
}
'use server'

import { cookies } from 'next/headers'

export async function exampleAction() {
  // Cookieを取得
  const value = cookies().get('name')?.value

  // Cookieを設定
  cookies().set('name', 'Delba')

  // Cookieを削除
  cookies().delete('name')
}

サーバーアクションからのCookie削除の追加例を参照してください。

セキュリティ

認証と認可

サーバーアクションは公開APIエンドポイントとして扱い、ユーザーがアクションを実行する権限があることを確認する必要があります。例えば:

app/actions.ts
'use server'

import { auth } from './lib'

export function addItem() {
  const { user } = auth()
  if (!user) {
    throw new Error('このアクションを実行するにはサインインが必要です')
  }

  // ...
}

クロージャと暗号化

コンポーネント内でサーバーアクションを定義すると、アクションが外側の関数のスコープにアクセスできるクロージャが作成されます。例えば、publishアクションはpublishVersion変数にアクセスできます:

export default function Page() {
  const publishVersion = await getLatestVersion();

  async function publish(formData: FormData) {
    "use server";
    if (publishVersion !== await getLatestVersion()) {
      throw new Error('公開ボタンを押してからバージョンが変更されました');
    }
    ...
  }

  return <button action={publish}>Publish</button>;
}
export default function Page() {
  const publishVersion = await getLatestVersion();

  async function publish() {
    "use server";
    if (publishVersion !== await getLatestVersion()) {
      throw new Error('公開ボタンを押してからバージョンが変更されました');
    }
    ...
  }

  return <button action={publish}>Publish</button>;
}

クロージャは、レンダリング時のデータのスナップショット(例:publishVersion)をキャプチャして、後でアクションが呼び出されたときに使用する必要がある場合に便利です。

ただし、これが発生するためには、キャプチャされた変数がクライアントに送信され、アクションが呼び出されたときにサーバーに戻されます。機密データがクライアントに公開されるのを防ぐため、Next.jsはクロージャ変数を自動的に暗号化します。新しい秘密鍵は、Next.jsアプリケーションがビルドされるたびに各アクションに対して生成されます。つまり、アクションは特定のビルドに対してのみ呼び出すことができます。

知っておくと良いこと: 機密値がクライアントに公開されるのを防ぐために、暗号化だけに依存することは推奨しません。代わりに、Reactのtaint APIを使用して、特定のデータがクライアントに送信されないように積極的に防止する必要があります。

暗号化キーの上書き(上級者向け)

Next.jsアプリケーションを複数のサーバーにセルフホストする場合、各サーバーインスタンスが異なる暗号化キーを持つ可能性があり、不整合が生じる可能性があります。

これを緩和するために、process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY環境変数を使用して暗号化キーを上書きできます。この変数を指定すると、暗号化キーがビルド間で永続化され、すべてのサーバーインスタンスが同じキーを使用します。

これは、複数のデプロイメント間で一貫した暗号化動作がアプリケーションにとって重要な高度な使用例です。キーローテーションや署名などの標準的なセキュリティプラクティスを考慮する必要があります。

知っておくと良いこと: VercelにデプロイされたNext.jsアプリケーションはこれを自動的に処理します。

許可されたオリジン(上級者向け)

サーバーアクションは<form>要素で呼び出せるため、CSRF攻撃に対して脆弱になる可能性があります。

内部的に、サーバーアクションはPOSTメソッドを使用し、このHTTPメソッドのみが呼び出しを許可されます。これにより、特にSameSite Cookieがデフォルトである現代のブラウザでは、ほとんどのCSRF脆弱性が防止されます。

追加の保護として、Next.jsのサーバーアクションはOriginヘッダーHostヘッダー(またはX-Forwarded-Host)と比較します。これらが一致しない場合、リクエストは中止されます。つまり、サーバーアクションは、それをホストするページと同じホストでのみ呼び出すことができます。

リバースプロキシや多層バックエンドアーキテクチャ(サーバーAPIが本番ドメインと異なる)を使用する大規模なアプリケーションでは、serverActions.allowedOriginsオプションを使用して安全なオリジンのリストを指定することを推奨します。このオプションは文字列の配列を受け取ります。

next.config.js
/** @type {import('next').NextConfig} */
module.exports = {
  experimental: {
    serverActions: {
      allowedOrigins: ['my-proxy.com', '*.my-proxy.com'],
    },
  },
}

セキュリティとサーバーアクションについて詳しく学んでください。

追加リソース

サーバーアクションに関する詳細は、以下のReactドキュメントを参照してください: