メインコンテンツまでスキップ

Server Actionsとミューテーション

Server Actionsはサーバー上で実行される非同期関数です。これは、Next.jsアプリケーションでフォームの送信とデータの変更を処理するために、Server Components及びClient Componentsで使用することができます。

🎥 Watch: App Router でフォームとミューテーションについて詳しく学ぶ → YouTube (10 分).

規約

Server ActionはReactの"use server"ディレクティブを使用して定義できます。非同期関数の先頭にディレクティブを配置して、その関数をServer Actionとしてマークするか、または別のファイルの先頭に配置して、そのファイルのすべてのエクスポートをServer Actionしてマークすることができます。

Server Components

Server Componentsでは、インライン関数レベルもしくはモジュールレベルで"use server"ディレクティブを使用できます。Server Actionをインラインにするには、関数本体の先頭に"use server"ディレクティブを追加します:

app/page.tsx
// Server Component
export default function Page() {
// Server Action
async function create() {
'use server'

// ...
}

return (
// ...
)
}

Client Components

Client Componentsでは、モジュールレベルで"use server"ディレクティブを使用しているアクションのみをインポートできます。

Client ComponentでServer Actionを呼び出すには、新しいファイルを作成し、そのファイルの先頭に"use server"ディレクティブを追加します。ファイル内の全ての関数は、Client ComponentsとServer Componentsの両方で再利用できるServer Actionとしてマークされます:

app/actions.ts
'use server'

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

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

また、Server ActionをpropとしてClient Componentに渡すこともできます:

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

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

Behavior

  • Server Actionsは<form>要素action属性を使用して呼び出すことができます。

    • Server Componentsはデフォルトでプログレッシブエンハンスメントをサポートしており、JavaScriptがまだロードされていないか無効になっていてもフォームは送信されます。
    • Client Componentsでは、JavaScriptがまだロードされていない場合、Server Actionsを呼び出すフォームは送信がキューイングされ、クライアントのハイドレーションが優先されます。
    • ハイドレーション後、ブラウザはフォームの送信時にリフレッシュされません。
  • Server Actionsは<form>に限定されず、イベントハンドラ、useEffect、サードパーティのライブラリ、および<button>などの他のフォーム要素から呼び出すことができます。

  • Server Actionsは、Next.jsのキャッシュおよび再検証アーキテクチャと統合されています。アクションが呼び出されると、Next.jsは単一のサーバーラウンドトリップで更新されたUIと新しいデータの両方を返すことができます。

  • 舞台裏では、アクションはPOSTメソッドを使用しており、POSTメソッドのみがServer Actionsを呼び出すことができます。

  • Server Actionsの引数および戻り値は、Reactによってシリアライズ可能である必要があります。シリアライズ可能な引数と返り値 のリストについては、Reactのドキュメントを参照してください。

  • Server Actionsは関数です。これは、それらをアプリケーション内のどこでも再利用できることを意味します。

  • Server Actionsは、使用されているページまたはレイアウトからランタイムを継承します。

  • Server Actionsは、使用されているページまたはレイアウトからのRoute Segment Configを継承します。これにはmaxDurationなどのフィールドも含まれます。

フォーム

ReactはHTMLの<form>要素を拡張し、actionpropを使用してServer Actionsを呼び出すことを可能にします。

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

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

Good to know:

追加の引数の渡し方

JavaScriptのbindメソッドを使用して、Server Actionに追加の引数を渡すことができます。

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

Server Actionは、フォームデータに加えて引数userIdを受け取ります:

app/actions.js
'use server'

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

Good to know:

  • 別の方法として、フォーム内の隠し入力フィールドとして引数を渡すことがあります(例: <input type="hidden" name="userId" value={userId} />)。ただし、この値は生成されたHTMLの一部となり、エンコードされません。
  • .bindはServer ComponentsとClient Componentsの両方で機能します。また、プログレッシブエンハンスメントもサポートしています。

保留中の状態

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

  • useFormStatusは特定の<form>に対するステータスを返すため、<form>要素の子として定義する必要があります
  • useFormStatusはReactフックであるため、Client Component内で使用する必要があります。
app/submit-button.tsx
'use client'

import { useFormStatus } from 'react-dom'

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

return (
<button type="submit" aria-disabled={pending}>
Add
</button>
)
}

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

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

// Server Component
export default async function Home() {
return (
<form action={createItem}>
<input type="text" name="field-name" />
<SubmitButton />
</form>
)
}

サーバーサイドのバリデーションとエラーハンドリング

基本的なクライアントサイドのフォームバリデーションには、requiredtype="email"などのHTMLバリデーションを使用することをお勧めします。

より高度なサーバーサイドのバリデーションには、データを変更する前にフォームフィールドを検証するためにzodのようなライブラリを使用することができます:

app/actions.ts
'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のuseFormStateフックを使用してユーザーにメッセージを表示できます。

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

export async function createUser(prevState: any, formData: FormData) {
// ...
return {
message: 'Please enter a valid email',
}
}

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

app/ui/signup.tsx
'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">Email</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite" className="sr-only">
{state?.message}
</p>
<button>Sign up</button>
</form>
)
}

Good to know:

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

楽観的な更新

Server Actionの完了を待つのではなく、UIを楽観的に更新するために、ReactのuseOptimisticフックを使用することができます:

app/page.tsx
'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[]>(
messages,
(state: Message[], newMessage: string) => [
...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">Send</button>
</form>
</div>
)
}

ネストした要素

<form>内のネストされた要素(例:<button><input type="submit">、および<input type="image">)でServer Actionを呼び出すことができます。これらの要素はformAction propまたはイベントハンドラを受け入れます。

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

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

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

app/entry.tsx
'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>の祖先の送信がトリガーされ、それによりServer Actionが呼び出されます。

フォーム以外の要素

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

イベントハンドラ

onClickなどのイベントハンドラからServer Actionを呼び出すことができます。例えば、likeのカウントを増やす場合:

app/like-button.tsx
'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>
</>
)
}

ユーザーエクスペリエンスを向上させるために、Server Actionがサーバー上で実行を終える前にUIを更新したり、保留中の状態を表示するために、useOptimisticuseTransitionなどの他のReactのAPIを使用することをお勧めします。

フォーム要素には、例えばフォームフィールドの変更時に保存するための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>
)
}

このような場合、複数のイベントが素早く連続して発生する可能性があるため、不要なServer Actionの呼び出しを防ぐためにデバウンシングをお勧めします。

useEffect

ReactのuseEffectフックを使用して、コンポーネントがマウントされたときや依存関係が変更されたときにServer Actionを呼び出すことができます。これは、グローバルなイベントに依存する変更や自動的にトリガーする必要がある変更に役立ちます。例えば、アプリケーションのショートカットのためのonKeyDown、無限スクロールのためのIntersection Observerフック、またはコンポーネントがマウントされたときにビューカウントを更新する場合などが挙げられます:

app/view-count.tsx
'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を使用することをお勧めします。

例えば、Server Actionは新しいアイテムを作成する際のエラーをメッセージで返すように処理するかもしれません:

app/actions.ts
'use server'

export async function createTodo(prevState: any, formData: FormData) {
try {
// Mutate data
} catch (e) {
throw new Error('Failed to create task')
}
}

Good to know:

データの再検証

Server Actions内でrevalidatePathAPIを使用して、Next.jsのキャッシュを再検証することができます:

app/actions.ts
'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')
}

リダイレクト

Server Actionの完了後にユーザーを別のルートにリダイレクトしたい場合、redirectAPIを使用できます。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') // Update cached posts
redirect(`/post/${id}`) // Navigate to the new post page
}

クッキー

Server Action内でcookiesAPIを使用して、クッキーをget(取得)、set(設定)、delete(削除)することができます。

app/actions.ts
'use server'

import { cookies } from 'next/headers'

export async function exampleAction() {
// Get cookie
const value = cookies().get('name')?.value

// Set cookie
cookies().set('name', 'Delba')

// Delete cookie
cookies().delete('name')
}

Server Actions内でクッキーを削除する追加の例については、関連する例をご覧ください。

セキュリティ

認証と認可

Server Actionsは、公開APIエンドポイントと同様に扱い、アクションを実行するためにユーザーが認可されていることを確認する必要があります。例:

app/actions.ts
'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')
}

// ...
}

クロージャと暗号化

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

app/page.tsx
export default function Page() {
const publishVersion = await getLatestVersion();

async function publish(formData: FormData) {
"use server";
if (publishVersion !== await getLatestVersion()) {
throw new Error('The version has changed since pressing publish');
}
...
}

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

アクションが呼び出されるとき、後で使用できるようにデータ(例:publishVersion)のスナップショットを取得する必要がある場合に、クロージャは役立ちます。

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

Good to know: 機密の値がクライアントで公開されるのを防ぐために、暗号化だけに依存することはお勧めしません。その代わりに、Reactのtaint APIを使用して、特定のデータがクライアントに送信されないよう積極的に防止するべきです。

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

複数のサーバーに横断して自分のNext.jsアプリケーションをホスティングする場合、各サーバーインスタンスは異なる暗号化キーを持つ可能性があり、潜在的な不整合が生じる可能性があります。

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

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

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

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

Server Actionsは<form>要素で呼び出すことができるため、これによりCSRF攻撃の対象になります。

Server Actionsは裏ではPOSTメソッドを使用し、POSTメソッドのみがServer Actionsを呼び出すことが許可されています。これにより、現代のブラウザではほとんどのCSRF脆弱性が防止されます。特にSameSiteクッキーがデフォルトの状態であることが重要です。

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

リバースプロキシやマルチレイヤーのバックエンドアーキテクチャ(サーバー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ドキュメントを参照してください: