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

Server Actions とデータの変更

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

🎥 視聴: Server Actionsを使用したデータの変更について詳しく学ぶ → YouTube (10分間)

規約

Server ActionはReactの"use server"ディレクティブで定義できます。任意の非同期関数の先頭にこのディレクティブを追加するか、別ファイルの先頭に追加してそのファイルのエクスポート全てをServer Actionsとしてマークできます。

Server Components

Server Componentはインライン機能レベルまたはモジュールレベルでの"use server"ディレクティブを利用できます。Server Actionをインラインで定義するには、関数本文のトップに"use server"を追加します。

app/page.tsx
export default function Page() {
// Server Action
async function create() {
'use server'
// データを変更する
}

return '...'
}

Client Components

Client Component内でServer Actionを呼び出すには、新しいファイルを作成し、トップに"use server"ディレクティブを追加します。ファイル内のすべてのエクスポートされた関数がServer Actionsとしてマークされ、Client ComponentとServer Componentの両方で再利用できます。

app/actions.ts
'use server'

export async function create() {}
app/ui/button.tsx
'use client'

import { create } from '@/app/actions'

export function Button() {
return <button onClick={() => create()}>Create</button>
}

アクションをpropsとして渡す

Server ActionをClient Componentのpropsとして渡すこともできます:

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

export default function ClientComponent({
updateItemAction,
}: {
updateItemAction: (formData: FormData) => void
}) {
return <form action={updateItemAction}>{/* ... */}</form>
}

通常、Next.js TypeScriptプラグインはclient-component.tsx内でupdateItemActionにフラグを立てますが、actionまたはActionで終わる名前を持つpropsはServer Actionsを受け取ると想定されます。これはあくまでヒューリスティックであり、TypeScriptプラグインは実際にServer Actionを受け取っているかどうかを知りませんが、実行時の型チェックはクライアントコンポーネントに誤って関数を渡さないようにします。

動作

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

フォーム

ReactはHTMLの<form>要素を拡張して、action propを使って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'),
}

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

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は、formDataに加えて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並びにClient Componentsで機能し、プログレッシブ・エンハンスメントをサポートしています。

入れ子になったフォーム要素

<button><input type="submit"><input type="image">などの<form>内にネストされた要素でもServer Actionを呼び出すことができます。これらの要素はformActionプロップまたはイベントハンドラを受け取ります。

これは、フォーム内で複数のサーバーアクションを呼び出したい場合に便利です。例として、投稿を公開するのに加え、下書きを保存するための特定の<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が呼び出されることになります。

サーバーサイドのフォーム検証

requiredtype="email"などのHTML属性を使用して基本的なクライアントサイドのフォーム検証ができます。

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

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

// データを変更する
}

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

  • アクションをuseFormStateに渡すことで、アクションの関数シグネチャが変更され、先頭の引数として新しいprevStateまたはinitialStateパラメータを受け取るようになります。
  • useFormStateはReactフックであるため、Client Component内で使用する必要があります。
app/actions.ts
'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: '有効なメールアドレスを入力してください' }
}

redirect('/dashboard')
}

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

app/ui/signup.tsx
'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>
)
}

Good to know:

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

ペンディング状態

  • データを変更する前に、ユーザーがアクションを実行する権限を持っていることも確認するべきです。認証と認可を参照してください。

useFormStatusフックはpendingというブール値を公開しており、これを利用してアクションが実行されている間にローディングインジケータを表示することができます。

app/submit-button.tsx
'use client'

import { useFormStatus } from 'react-dom'

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

return (
<button disabled={pending} type="submit">
Sign Up
</button>
)
}

Good to know:

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

楽観的更新

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[],
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
'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で保存します:

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を使用する場合や、無限スクロールのための交差オブザーバーフック、もしくはコンポーネントのマウント時にビュー数を更新する場合などに適しています。

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 {
// データを変更する
} catch (e) {
throw new Error('タスクの作成に失敗しました')
}
}

Good to know:

データの再検証

Server Actions内でrevalidatePath APIを使ってNext.js Cacheを再検証することができます。

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の完了後にユーザーを別のルートにリダイレクトしたい場合は、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}`) // 新しい投稿ページに移動する
}

Cookie

Server Action内でcookies APIを使用して、Cookieを取得、設定、削除することができます。

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')
}

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

セキュリティ

デフォルトでは、Server Actionが作成されてエクスポートされると、パブリックなHTTPエンドポイントが作成され、同等のセキュリティ仮定および承認チェックで扱われる必要があります。つまり、Server Actionやユーティリティ関数がコード内の他の場所でインポートされていない場合でも、それは依然としてパブリックにアクセス可能です。

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

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

Good to know:

IDはコンパイル時に作成され、最大14日間キャッシュされます。新しいビルドが開始されるか、ビルドキャッシュが無効化されると再生成されます。このセキュリティ強化により、認証層が欠如している場合のリスクが低減されます。ただし、Server Actionsはパブリック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
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アプリケーションがビルドされる度にアクションごとに生成されます。つまり、アクションは特定のビルドに対してのみ呼び出すことができます。

Good to know: 機密値がクライアントで公開されるのを防ぐための手段として暗号化だけに頼ることはお勧めしません。代わりに、Reactの汚染APIを使用して、特定のデータがクライアントに送信されるのを積極的に防ぐことを推奨します。

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

複数のサーバーにわたってNext.jsアプリケーションをセルフホスティングすると、各サーバーインスタンスが異なる暗号化キーを保持する可能性があり、潜在的な不一致につながる可能性があります。

これを軽減するには、process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY環境変数を使用して暗号化キーを上書きできます。この変数を指定することで、暗号化キーの一貫性がビルド間で維持され、すべてのサーバーインスタンスが同じキーを使用することが保証されます。

これは、複数のデプロイメントにわたって一貫した暗号化動作がアプリケーションにとって重要な場合の高度なユースケースです。キーの回転や署名などの標準的なセキュリティプラクティスを考慮するべきです。

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

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

Server Actionsは<form>要素で呼び出されるため、CSRF攻撃に晒される可能性があります。

裏では、Server ActionsはPOSTメソッドを使用しており、このHTTPメソッドのみがそれを呼び出すことができます。これにより、特にSameSite cookieがデフォルトである現代のブラウザにおける多くのCSRF脆弱性が防止されます。

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

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

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

セキュリティとServer Actionsについて詳しく学びましょう。

追加リソース

詳細については、以下のReactドキュメントを参照してください: