サーバーアクションとミューテーション
サーバーアクションは、サーバー上で実行される非同期関数です。これはサーバーコンポーネントとクライアントコンポーネントの両方で呼び出すことができ、Next.jsアプリケーションでフォーム送信とデータミューテーションを処理します。
🎥 視聴する: サーバーアクションを使ったミューテーションについて詳しく学ぶ → YouTube (10分)。
規約
サーバーアクションはReactの"use server"
ディレクティブを使って定義できます。async
関数の先頭にこのディレクティブを配置して関数をサーバーアクションとしてマークするか、別のファイルの先頭に配置してそのファイルのすべてのエクスポートをサーバーアクションとしてマークできます。
サーバーコンポーネント
サーバーコンポーネントは関数レベルまたはモジュールレベルの"use server"
ディレクティブを使用できます。サーバーアクションをインラインで定義するには、関数本体の先頭に"use server"
を追加します:
export default function Page() {
// Server Action
async function create() {
'use server'
// Mutate data
}
return '...'
}
クライアントコンポーネント
クライアントコンポーネントでサーバーアクションを呼び出すには、新しいファイルを作成してその先頭に"use server"
ディレクティブを追加します。このファイル内でエクスポートされるすべての関数は、クライアントコンポーネントとサーバーコンポーネントの両方で再利用できるサーバーアクションとしてマークされます:
'use server'
export async function create() {}
'use client'
import { create } from './actions'
export function Button() {
return <button onClick={() => create()}>Create</button>
}
アクションをpropsとして渡す
サーバーアクションをクライアントコンポーネントにpropsとして渡すこともできます:
<ClientComponent updateItemAction={updateItem} />
'use client'
export default function ClientComponent({
updateItemAction,
}: {
updateItemAction: (formData: FormData) => void
}) {
return <form action={updateItemAction}>{/* ... */}</form>
}
通常、Next.jsのTypeScriptプラグインはclient-component.tsx
内のupdateItemAction
にフラグを立てます。これは関数が一般的にクライアント-サーバー間の境界をシリアライズできないためです。
しかし、action
という名前のpropsやAction
で終わる名前のpropsはサーバーアクションを受け取ると想定されます。
これはヒューリスティックなものであり、TypeScriptプラグインは実際にサーバーアクションなのか通常の関数なのかを知ることができません。
ランタイムの型チェックにより、誤って関数をクライアントコンポーネントに渡すことを防ぎます。
動作
- サーバーアクションは
<form>
要素のaction
属性を使って呼び出すことができます:- サーバーコンポーネントはデフォルトでプログレッシブエンハンスメントをサポートしているため、JavaScriptがまだ読み込まれていない場合やJavaScriptが無効になっている場合でもフォームは送信されます。
- クライアントコンポーネントでは、サーバーアクションを呼び出すフォームはJavaScriptがまだ読み込まれていない場合、クライアントハイドレーションを優先して送信をキューに入れます。
- ハイドレーション後、フォーム送信時にブラウザは更新されません。
- サーバーアクションは
<form>
に限定されず、イベントハンドラー、useEffect
、サードパーティライブラリ、および<button>
などの他のフォーム要素からも呼び出すことができます。 - サーバーアクションはNext.jsのキャッシングと再検証アーキテクチャと統合されています。アクションが呼び出されると、Next.jsは更新されたUIと新しいデータを1回のサーバーラウンドトリップで返すことができます。
- 内部的に、アクションは
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'),
}
// mutate data
// revalidate cache
}
return <form action={createInvoice}>...</form>
}
補足:
- 例:読み込み状態とエラー状態を持つフォーム
- 多くのフィールドを持つフォームを扱う場合は、JavaScriptの
Object.fromEntries()
とentries()
メソッドの使用を検討するとよいでしょう。例:const rawFormData = Object.fromEntries(formData)
。ただし、formData
には追加の$ACTION_
プロパティが含まれることに注意してください。- 詳細はReactの
<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">Update User Name</button>
</form>
)
}
サーバーアクションはフォームデータに加えてuserId
引数を受け取ります:
'use server'
export async function updateUser(userId: string, formData: 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
イベントをリッスンできます:
'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>
祖先の送信がトリガーされ、サーバーアクションが呼び出されます。
サーバー側のフォームバリデーション
基本的なクライアント側のフォームバリデーションには、required
やtype="email"
などのHTML属性を使用できます。
より高度なサーバー側バリデーションには、zodのようなライブラリを使用して、データを変更する前にフォームフィールドを検証できます:
'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'),
})
// Return early if the form data is invalid
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
// Mutate data
}
フィールドがサーバー上で検証されたら、アクションでシリアライズ可能なオブジェクトを返し、React useActionState
フックを使用してユーザーにメッセージを表示できます。
- アクションを
useActionState
に渡すと、アクションの関数シグネチャが変わり、最初の引数として新しいprevState
またはinitialState
パラメータを受け取るようになります。 useActionState
はReactフックであるため、クライアントコンポーネントで使用する必要があります。
'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')
}
そして、アクションをuseActionState
フックに渡し、返されたstate
を使用してエラーメッセージを表示できます。
'use client'
import { useActionState } from 'react'
import { createUser } from '@/app/actions'
const initialState = {
message: '',
}
export function Signup() {
const [state, formAction, pending] = useActionState(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 disabled={pending}>Sign up</button>
</form>
)
}
保留状態
useActionState
フックは、アクションの実行中に読み込みインジケーターを表示するために使用できるpending
ブール値を公開しています。
または、useFormStatus
フックを使用して、アクションの実行中に読み込みインジケーターを表示することもできます。このフックを使用する場合は、読み込みインジケーターを表示するための別のコンポーネントを作成する必要があります。例えば、アクションが保留中の場合にボタンを無効にするには:
'use client'
import { useFormStatus } from 'react-dom'
export function SubmitButton() {
const { pending } = useFormStatus()
return (
<button disabled={pending} type="submit">
Sign Up
</button>
)
}
その後、SubmitButton
コンポーネントをフォーム内にネストできます:
import { SubmitButton } from './button'
import { createUser } from '@/app/actions'
export function Signup() {
return (
<form action={createUser}>
{/* Other form elements */}
<SubmitButton />
</form>
)
}
補足: React 19では、
useFormStatus
は返されるオブジェクトにdata、method、actionなどの追加のキーが含まれています。React 19を使用していない場合は、pending
キーのみが利用可能です。
楽観的更新
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 }])
const formAction = async (formData: 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>
)
}
イベントハンドラー
サーバーアクションは<form>
要素内で使用されることが一般的ですが、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>
</>
)
}
フォーム要素にイベントハンドラーを追加することもできます。例えば、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 } 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>
境界によってキャッチされます。詳細については、エラー処理を参照してください。
補足:
- エラーをスローする以外に、
useActionState
で処理されるオブジェクトを返すこともできます。サーバー側のバリデーションとエラー処理を参照してください。
データの再検証
サーバーアクション内でrevalidatePath
APIを使用して、Next.jsキャッシュを再検証できます:
'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')
}
リダイレクト
サーバーアクションの完了後にユーザーを別のルートにリダイレクトしたい場合は、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}`) // 新しい投稿ページに移動
}
クッキー
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')
}
サーバーアクションからクッキーを削除する追加の例については、ドキュメントを参照してください。
セキュリティ
デフォルトでは、サーバーアクションが作成されてエクスポートされると、パブリックなHTTPエンドポイントが作成され、同じセキュリティ上の前提と認証チェックで扱う必要があります。これは、サーバーアクションやユーティリティ関数がコード内の他の場所でインポートされていなくても、公開的にアクセス可能であることを意味します。
セキュリティを向上させるため、Next.jsには以下の組み込み機能があります:
- 安全なアクションID: Next.jsは、クライアントがサーバーアクションを参照して呼び出すことができるように、暗号化された非決定論的なIDを作成します。これらのIDはセキュリティを強化するために、ビルド間で定期的に再計算されます。
- デッドコード除去: 未使用のサーバーアクション(IDで参照される)はクライアントバンドルから削除され、サードパーティによる公開アクセスを防ぎます。
補足:
IDはコンパイル時に作成され、最大14日間キャッシュされます。新しいビルドが開始されるか、ビルドキャッシュが無効化されると、IDは再生成されます。 このセキュリティ向上により、認証レイヤーが欠けている場合のリスクが軽減されます。ただし、サーバーアクションをパブリックな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('このアクションを実行するにはサインインが必要です')
}
// ...
}
クロージャーと暗号化
コンポーネント内でサーバーアクションを定義すると、アクションが外部関数のスコープにアクセスできるクロージャーが作成されます。例えば、publish
アクションはpublishVersion
変数にアクセスできます:
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}>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クッキーがデフォルトである最新のブラウザでは、ほとんどの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ドキュメントを参照してください: