Menu

サーバーアクションとミューテーション

サーバーアクションは、サーバー上で実行される非同期関数です。Next.jsアプリケーションでフォーム送信とデータ変更を処理するため、サーバーコンポーネントとクライアントコンポーネントで呼び出すことができます。

🎥 ウォッチ: サーバーアクションを使用したミューテーションについてさらに詳しく → YouTube (10分)

規約

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

サーバーコンポーネント

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

app/page.tsx
TypeScript
export default function Page() {
  // サーバーアクション
  async function create() {
    'use server'
    // データを変更
  }
 
  return '...'
}

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

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

app/actions.ts
TypeScript
'use server'
 
export async function create() {}
app/actions.js
'use server'
 
export async function create() {}
app/ui/button.tsx
TypeScript
'use client'
 
import { create } from '@/app/actions'
 
export function Button() {
  return <button onClick={() => create()}>作成</button>
}

アクションをプロップとして渡す

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

<ClientComponent updateItemAction={updateItem} />
app/client-component.tsx
TypeScript
'use client'
 
export default function ClientComponent({
  updateItemAction,
}: {
  updateItemAction: (formData: FormData) => void
}) {
  return <form action={updateItemAction}>{/* ... */}</form>
}

通常、Next.jsのTypeScriptプラグインは、一般的にシリアライズできない関数である updateItemAction にフラグを立てます。 しかし、action という名前のプロップや、Action で終わるプロップは、サーバーアクションを受け取ると想定されます。 これは、TypeScriptプラグインが実際にサーバーアクションか通常の関数かを知らないヒューリスティックにすぎません。 ランタイムの型チェックにより、クライアントコンポーネントに誤って関数を渡すことはできません。

動作

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

フォーム

ReactはHTML <form> 要素を拡張し、action プロップを使用してサーバーアクションを呼び出すことができます。

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

app/invoices/page.tsx
TypeScript
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>
}

補足:

追加の引数を渡す

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

app/client-component.tsx
TypeScript
'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>
  )
}

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

app/actions.js
'use server'
 
export async function updateUser(userId, formData) {}

補足:

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

ネストされたフォーム要素

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

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

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

requestSubmit()メソッドを使用して、プログラムによりフォーム送信をトリガーできます。例えば、ユーザーが + Enterキーボードショートカットでフォームを送信する場合、onKeyDownイベントをリスンできます:

app/entry.tsx
TypeScript
'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>
  )
}

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

サーバー側のフォーム検証

requiredtype="email"などのHTML属性を使用して、基本的なクライアント側のフォーム検証を行えます。

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

app/actions.ts
TypeScript
'use server'
 
import { z } from 'zod'
 
const schema = z.object({
  email: z.string({
    invalid_type_error: 'Invalid Email',
  }),
})
 
export default async function createUser(formData: 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のフックであるため、クライアントコンポーネント内で使用する必要があります。
app/actions.ts
TypeScript
'use server'
 
import { redirect } from 'next/navigation'
 
export async function createUser(prevState: any, formData: FormData) {
  const res = await fetch('https://...')
  const json = await res.json()
 
  if (!res.ok) {
    return { message: 'Please enter a valid email' }
  }
 
  redirect('/dashboard')
}

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

app/ui/signup.tsx
TypeScript
'use client'
 
import { useFormState } from 'react'
import { createUser } from '@/app/actions'
 
const initialState = {
  message: '',
}
 
export function Signup() {
  const [state, formAction] = useFormState(createUser, initialState)
 
  return (
    <form action={formAction}>
      <label htmlFor="email">Email</label>
      <input type="text" id="email" name="email" required />
      {/* ... */}
      <p aria-live="polite">{state?.message}</p>
      <button>Sign up</button>
    </form>
  )
}

補足:

  • これらの例では、Next.jsのApp RouterにバンドルされているReactのuseFormStateフックを使用しています。React 19を使用している場合は、代わりにuseActionStateを使用してください。詳細については、Reactのドキュメントを参照してください。

保留状態

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

useFormStatusフックは、アクションの実行中にローディングインジケータを表示するために使用できるpendingブール値を公開します。

app/submit-button.tsx
TypeScript
'use client'
 
import { useFormStatus } from 'react-dom'
 
export function SubmitButton() {
  const { pending } = useFormStatus()
 
  return (
    <button disabled={pending} type="submit">
      Sign Up
    </button>
  )
}

補足:

  • React 19では、useFormStatusは返されるオブジェクトに追加のキー(data、method、actionなど)が含まれます。React 19を使用していない場合、pendingキーのみが利用可能です。
  • React 19では、useActionStateも返される状態にpendingキーが含まれます。

楽観的な更新

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

app/page.tsx
TypeScript
'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 }])
 
  const formAction = async (formData) => {
    const message = formData.get('message') as string
    addOptimisticMessage(message)
    await send(message)
  }
 
  return (
    <div>
      {optimisticMessages.map((m, i) => (
        <div key={i}>{m.message}</div>
      ))}
      <form action={formAction}>
        <input type="text" name="message" />
        <button type="submit">Send</button>
      </form>
    </div>
  )
}

イベントハンドラ

Server Actionsは通常<form>要素内で使用されますが、onClickなどのイベントハンドラからも呼び出すことができます。例えば、いいねのカウントを増やす場合:

app/like-button.tsx
TypeScript
'use client'
 
import { incrementLike } from './actions'
import { useState } from 'react'
 
export default function LikeButton({ initialLikes }: { initialLikes: number }) {
  const [likes, setLikes] = useState(initialLikes)
 
  return (
    <>
      <p>総いいね数: {likes}</p>
      <button
        onClick={async () => {
          const updatedLikes = await incrementLike()
          setLikes(updatedLikes)
        }}
      >
        いいね
      </button>
    </>
  )
}

また、フォーム要素にイベントハンドラを追加することもできます。例えば、フォームフィールドの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">公開</button>
    </form>
  )
}

このようなケースでは、短時間に複数のイベントが発生する可能性があるため、不必要なServer Actionの呼び出しを防ぐためにデバウンスを推奨します。

useEffect

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

app/view-count.tsx
TypeScript
'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>総ビュー数: {views}</p>
}

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

エラー処理

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

例えば、Server Actionで新しいアイテムの作成時のエラーをメッセージで処理できます:

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

補足:

データの再検証

Server Actions内でrevalidatePath APIを使用して、Next.jsのキャッシュを再検証できます:

app/actions.ts
'use server'
 
import { revalidatePath } from 'next/cache'
 
export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }
 
  revalidatePath('/posts')
}
app/actions.js
'use server'
 
import { revalidatePath } from 'next/cache'
 
export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }
 
  revalidatePath('/posts')
}

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

app/actions.ts
'use server'
 
import { revalidateTag } from 'next/cache'
 
export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }
 
  revalidateTag('posts')
}
app/actions.js
'use server'
 
import { revalidateTag } from 'next/cache'
 
export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }
 
  revalidateTag('posts')
}

リダイレクト

Server Action完了後にユーザーを別のルートにリダイレクトする場合は、redirect APIを使用できます。redirecttry/catchブロックの外で呼び出す必要があります:

app/actions.ts
'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}`) // 新しい投稿ページに移動
}
app/actions.js
'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

Server Action内でcookies APIを使用してCookieをgetsetdeleteできます:

app/actions.ts
'use server'
 
import { cookies } from 'next/headers'
 
export async function exampleAction() {
  const cookieStore = await cookies()
 
  // Cookieを取得
  cookieStore.get('name')?.value
 
  // Cookieを設定
  cookieStore.set('name', 'Delba')
 
  // Cookieを削除
  cookieStore.delete('name')
}
app/actions.js
'use server'
 
import { cookies } from 'next/headers'
 
export async function exampleAction() {
  // Cookieを取得
  const cookieStore = await cookies()
 
  // Cookieを取得
  cookieStore.get('name')?.value
 
  // Cookieを設定
  cookieStore.set('name', 'Delba')
 
  // Cookieを削除
  cookieStore.delete('name')
}

Server ActionsからCookieを削除する追加の例を参照してください。

セキュリティ

デフォルトでは、Server Actionの作成とエクスポート時に、公開HTTPエンドポイントが作成され、同じセキュリティの前提条件と認証チェックを受けます。つまり、Server Actionまたはユーティリティ関数がコード内の他の場所でインポートされていなくても、依然として公開アクセス可能です。

セキュリティを向上させるために、Next.jsには以下の組み込み機能があります:

  • セキュアなアクションID: Next.jsは、クライアントがServer Actionを参照および呼び出すための、暗号化された非決定的なIDを作成します。これらのIDはビルド間で定期的に再計算され、セキュリティが強化されます。
  • デッドコード除去: 未使用のServer Actions(そのIDによって参照される)はクライアントバンドルから削除され、サードパーティによる公開アクセスを防ぎます。

補足:

IDは、コンパイル時に作成され、最大14日間キャッシュされます。新しいビルドが開始されるか、ビルドキャッシュが無効化されると、再生成されます。 このセキュリティ改善は、認証レイヤーが欠落している場合のリスクを軽減します。ただし、Server Actionは依然としてパブリックHTTPエンドポイントとして扱うべきです。

// app/actions.js
'use server'
 
// このアクションはアプリケーションで使用されるため、Next.jsは
// クライアントがServer Actionを参照および呼び出すための
// セキュアなIDを作成します。
export async function updateUserAction(formData) {}
 
// このアクションはアプリケーションで使用されないため、Next.js
// は`next build`中にこのコードを自動的に削除し、
// パブリックエンドポイントを作成しません。
export async function deleteUserAction(formData) {}

認証と承認

ユーザーがアクションを実行する権限があることを確認する必要があります。例:

app/actions.ts
'use server'
 
import { auth } from './lib'
 
export function addItem() {
  const { user } = auth()
  if (!user) {
    throw new Error('このアクションを実行するにはサインインする必要があります')
  }
 
  // ...
}

クロージャと暗号化

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

app/page.tsx
TypeScript
export default async function Page() {
  const publishVersion = await getLatestVersion();
 
  async function publish() {
    "use server";
    if (publishVersion !== await getLatestVersion()) {
      throw new Error('公開ボタンを押してから、バージョンが変更されました');
    }
    ...
  }
 
  return (
    <form>
      <button formAction={publish}>公開</button>
    </form>
  );
}

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

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

補足: 機密値がクライアントに公開されるのを防ぐために暗号化だけに頼ることはお勧めしません。代わりに、Reactタイント APIを使用して、特定のデータがクライアントに送信されるのを積極的に防ぐ必要があります。

暗号化キーの上書き(高度)

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

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

これは、複数のデプロイメント間で一貫した暗号化動作が重要なアプリケーションの高度なユースケースです。キーのローテーションや署名などの標準的なセキュリティ慣行を検討する必要があります。

補足: VercelにデプロイされたNext.jsアプリケーションは、これを自動的に処理します。

許可されたオリジン(高度)

Server Actionは<form>要素で呼び出すことができるため、CSRFアタックに対して脆弱になります。

バックグラウンドでは、Server ActionはPOSTメソッドを使用し、このHTTPメソッドのみが呼び出しを許可されます。これにより、最新のブラウザで、特にSameSiteクッキーがデフォルトであることで、ほとんどのCSRF脆弱性を防ぎます。

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

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

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

Server ActionsのセキュリティとServer Actionsの詳細をご覧ください。

その他のリソース

詳細については、以下のReactドキュメントをご確認ください:

下一步

Next.jsでサーバーアクションを設定する方法を学びます