サーバーアクションとデータ変更
サーバーアクション (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'
export async function create() {}
'use server'
export async function create() {}
アクションをプロパティとして渡す
サーバーアクションをクライアントコンポーネントにプロパティとして渡すこともできます:
<ClientComponent updateItemAction={updateItem} />
'use client'
export default function ClientComponent({
updateItemAction,
}: {
updateItemAction: (formData: FormData) => void
}) {
return <form action={updateItemAction}>{/* ... */}</form>
}
'use client'
export default function ClientComponent({ updateItemAction }) {
return <form action={updateItemAction}>{/* ... */}</form>
}
通常、Next.js の TypeScript プラグインは、client-component.tsx
の updateItemAction
をフラグします。これは、一般的にクライアント-サーバー間の境界を越えてシリアライズできない関数だからです。しかし、action
という名前のプロパティや Action
で終わるプロパティは、サーバーアクションを受け取ると想定されます。これは、TypeScript プラグインが実際にサーバーアクションか通常の関数を受け取るかを知らないため、ヒューリスティックに過ぎません。実行時の型チェックは、誤って関数をクライアントコンポーネントに渡さないようにします。
動作
- サーバーアクションは
<form>
要素のaction
属性を使用して呼び出すことができます。- サーバーコンポーネントはデフォルトでプログレッシブエンハンスメントをサポートしており、JavaScript がまだロードされていないか無効になっている場合でもフォームが送信されます。
- クライアントコンポーネントでは、サーバーアクションを呼び出すフォームは、JavaScript がロードされていない場合に送信をキューに入れ、クライアントのハイドレーションを優先します。
- ハイドレーション後、フォーム送信時にブラウザはリフレッシュされません。
- サーバーアクションは
<form>
に限定されず、イベントハンドラー、useEffect
、サードパーティライブラリ、および<button>
などの他のフォーム要素から呼び出すことができます。 - サーバーアクションは Next.js のキャッシュと再検証アーキテクチャと統合されています。アクションが呼び出されると、Next.js は更新された UI と新しいデータを単一のサーバーラウンドトリップで返すことができます。
- 内部的には、アクションは
POST
メソッドを使用し、この HTTP メソッドのみがアクションを呼び出すことができます。 - サーバーアクションの引数と戻り値は、React によってシリアライズ可能でなければなりません。シリアライズ可能な引数と値のリストについては、React ドキュメントを参照してください。
- サーバーアクションは関数です。つまり、アプリケーションのどこでも再利用できます。
- サーバーアクションは、使用されるページまたはレイアウトからランタイムを継承します。
- サーバーアクションは、使用されるページまたはレイアウトからルートセグメント設定を継承し、
maxDuration
などのフィールドを含みます。
例
イベントハンドラー
サーバーアクションを <form>
要素内で使用するのが一般的ですが、onClick
などのイベントハンドラーでも呼び出すことができます。例えば、いいねカウントを増やす場合:
フォーム要素にイベントハンドラーを追加することもできます。例えば、フォームフィールドを onChange
で保存する場合:
'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, useTransition } from 'react'
export default function ViewCount({ initialViews }: { initialViews: number }) {
const [views, setViews] = useState(initialViews)
const [isPending, startTransition] = useTransition()
useEffect(() => {
startTransition(async () => {
const updatedViews = await incrementViews()
setViews(updatedViews)
})
}, [])
// `isPending` を使用してユーザーにフィードバックを提供できます
return <p>Total Views: {views}</p>
}
'use client'
import { incrementViews } from './actions'
import { useState, useEffect, useTransition } from 'react'
export default function ViewCount({ initialViews }) {
const [views, setViews] = useState(initialViews)
const [isPending, startTransition] = useTransition()
useEffect(() => {
starTransition(async () => {
const updatedViews = await incrementViews()
setViews(updatedViews)
})
}, [])
// `isPending` を使用してユーザーにフィードバックを提供できます
return <p>Total Views: {views}</p>
}
useEffect
の動作と注意点を考慮することを忘れないでください。
エラーハンドリング
エラーがスローされると、クライアント上の最も近い error.js
または <Suspense>
境界によってキャッチされます。詳細については、エラーハンドリングを参照してください。
知っておくと良い:
- エラーをスローする以外に、
useActionState
で処理されるオブジェクトを返すこともできます。
データの再検証
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 を使用できます。redirect
は try/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 を使用して、サーバーアクション内でクッキーを get
、set
、delete
できます:
'use server'
import { cookies } from 'next/headers'
export async function exampleAction() {
const cookieStore = await cookies()
// クッキーを取得
cookieStore.get('name')?.value
// クッキーを設定
cookieStore.set('name', 'Delba')
// クッキーを削除
cookieStore.delete('name')
}
'use server'
import { cookies } from 'next/headers'
export async function exampleAction() {
// クッキーを取得
const cookieStore = await cookies()
// クッキーを取得
cookieStore.get('name')?.value
// クッキーを設定
cookieStore.set('name', 'Delba')
// クッキーを削除
cookieStore.delete('name')
}
サーバーアクションからクッキーを削除する追加の例を参照してください。
セキュリティ
デフォルトでは、サーバーアクションが作成されてエクスポートされると、公開 HTTP エンドポイントが作成され、同じセキュリティの前提と認証チェックで扱う必要があります。これは、サーバーアクションやユーティリティ関数がコードの他の場所でインポートされていない場合でも、公開アクセス可能であることを意味します。
セキュリティを向上させるために、Next.js には次の組み込み機能があります:
- 安全なアクション ID: Next.js は、クライアントがサーバーアクションを参照して呼び出すことができるように、暗号化された非決定論的 ID を作成します。これらの ID は、セキュリティを強化するためにビルド間で定期的に再計算されます。
- デッドコード削除: 使用されていないサーバーアクション(ID で参照される)は、サードパーティによる公開アクセスを避けるためにクライアントバンドルから削除されます。
知っておくと良い:
ID はコンパイル中に作成され、最大 14 日間キャッシュされます。新しいビルドが開始されたときやビルドキャッシュが無効になったときに再生成されます。 このセキュリティ改善により、認証レイヤーが欠落している場合のリスクが減少します。ただし、サーバーアクションは公開 HTTP エンドポイントのように扱う必要があります。
// app/actions.js
'use server'
// このアクションはアプリケーションで使用されるため、Next.js は
// クライアントがサーバーアクションを参照して呼び出すことができる
// 安全な ID を作成します。
export async function updateUserAction(formData) {}
// このアクションはアプリケーションで使用されないため、Next.js は
// `next build` 中にこのコードを自動的に削除し、
// 公開エンドポイントを作成しません。
export async function deleteUserAction(formData) {}
認証と認可
ユーザーがアクションを実行する権限があることを確認する必要があります。例えば:
'use server'
import { auth } from './lib'
export function addItem() {
const { user } = auth()
if (!user) {
throw new Error('You must be signed in to perform this action')
}
// ...
}
クロージャーと暗号化
コンポーネント内でサーバーアクションを定義すると、アクションが外側の関数のスコープにアクセスできるクロージャーが作成されます。例えば、publish
アクションは publishVersion
変数にアクセスできます:
export default async function Page() {
const publishVersion = await getLatestVersion();
async function publish() {
"use server";
if (publishVersion !== await getLatestVersion()) {
throw new Error('The version has changed since pressing publish');
}
...
}
return (
<form>
<button formAction={publish}>Publish</button>
</form>
);
}
export default async function Page() {
const publishVersion = await getLatestVersion();
async function publish() {
"use server";
if (publishVersion !== await getLatestVersion()) {
throw new Error('The version has changed since pressing publish');
}
...
}
return (
<form>
<button formAction={publish}>Publish</button>
</form>
);
}
クロージャーは、データのスナップショット(例: publishVersion
)をレンダリング時にキャプチャして、後でアクションが呼び出されたときに使用できるようにする必要がある場合に便利です。
ただし、これが起こるためには、キャプチャされた変数がクライアントに送信され、アクションが呼び出されたときにサーバーに戻されます。クライアントに機密データが公開されるのを防ぐために、Next.js は自動的にクローズドオーバー変数を暗号化します。新しい秘密鍵は、Next.js アプリケーションがビルドされるたびにアクションごとに生成されます。これは、アクションが特定のビルドに対してのみ呼び出せることを意味します。
知っておくと良い: 機密値がクライアントに公開されるのを防ぐために、暗号化だけに頼ることはお勧めしません。代わりに、React の taint API を使用して、特定のデータがクライアントに送信されるのを事前に防ぐ必要があります。
暗号化キーの上書き(上級者向け)
Next.js アプリケーションを複数のサーバーでセルフホスティングする場合、各サーバーインスタンスが異なる暗号化キーを持つ可能性があり、不整合が生じる恐れがあります。
これを防ぐには、環境変数 process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
を使用して暗号化キーを上書きできます。この変数を指定することで、ビルド間で暗号化キーが永続化され、すべてのサーバーインスタンスが同じキーを使用するようになります。この変数は 必ず AES-GCM で暗号化されている必要があります。
これは、複数のデプロイメント間で一貫した暗号化動作がアプリケーションにとって重要な高度なユースケースです。キーローテーションや署名といった標準的なセキュリティ対策を検討する必要があります。
豆知識: Vercel にデプロイされた Next.js アプリケーションでは、これが自動的に処理されます。
許可されたオリジン(上級者向け)
サーバーアクションは <form>
要素内で呼び出せるため、CSRF 攻撃のリスクにさらされます。
内部的に、サーバーアクションは POST
メソッドを使用しており、この HTTP メソッドのみが呼び出しを許可されています。これにより、特に SameSite Cookie がデフォルトとなっている現代のブラウザでは、ほとんどの CSRF 脆弱性が防止されます。
追加の保護策として、Next.js のサーバーアクションは Origin ヘッダー と Host ヘッダー(または X-Forwarded-Host
)を比較します。これらが一致しない場合、リクエストは中止されます。つまり、サーバーアクションは、それをホストするページと同じホストでのみ呼び出せます。
リバースプロキシや多層バックエンドアーキテクチャ(サーバー API と本番ドメインが異なる場合)を使用する大規模アプリケーションでは、安全なオリジンのリストを指定するために serverActions.allowedOrigins
設定オプションを使用することを推奨します。このオプションは文字列の配列を受け入れます。
/** @type {import('next').NextConfig} */
module.exports = {
experimental: {
serverActions: {
allowedOrigins: ['my-proxy.com', '*.my-proxy.com'],
},
},
}
セキュリティとサーバーアクションについて詳しく学びましょう。
追加リソース
詳細については、以下の React ドキュメントを参照してください: