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

Server Actions とデータ変更

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

🎥 視聴: Server Actions を用いたデータ変更について詳しく学びましょう → YouTube (10 分)

規約

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

Server Components

Server Components は、インライン関数レベルまたはモジュールレベルで"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」ディレクティブを追加します。そのファイル内でエクスポートされるすべての関数が、ClientとServer Componentの両方で再利用可能なServer Actionとしてマークされます。

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.tsxupdateItemAction を警告しますが、これは一般にクライアント側とサーバー側の境界を越えてシリアル化できない関数だからです。しかし、action として名前付けされた props または Action で終了する props は Server Actions を受け取ることが想定されています。これはあくまでヒューリスティックであり、TypeScriptプラグインは実際に Server Action を受け取っているか、通常の関数を受け取っているかはわかりません。ランタイム型チェックにより、間違って関数をClient Componentに渡さないようにします。

行動

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

フォーム

ReactはHTMLの<form>要素を拡張し、actionプロップで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:

  • 例:Form with Loading & Error States
  • 多くのフィールドを持つフォームを使う場合、JavaScriptのObject.fromEntries()メソッドと共にentries()メソッドを使用することを検討してください。例えば、const rawFormData = Object.fromEntries(formData) となります。一点注意すべきは、formDataには追加の$ACTION_プロパティが含まれることです。
  • 詳しくは、React <form> ドキュメントを参照してください。

追加の引数を渡す

Server Action に追加の引数を渡すには、JavaScriptのbindメソッドを使用します。

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.ts
'use server'

export async function updateUser(userId: string, formData: FormData) {}

Good to know:

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

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

Server Action を <form> 内にネストされた要素、例えば <button><input type="submit"><input type="image"> で呼び出すこともできます。これらの要素は 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: '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のフックであるため、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: 'Please enter a valid email' }
}

redirect('/dashboard')
}

次に、アクションを 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">{state?.message}</p>
<button>Sign up</button>
</form>
)
}

Good to know:

  • これらの例は、Next.jsのApp RouterにバンドルされているReactのuseFormStateフックを使用します。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は返されるオブジェクトに、データ、メソッド、アクションのようにさらに多くのキーを含みます。React 19を使用していない場合、pendingキーのみが利用可能です。
  • React 19では、useActionStateも返される状態にpendingキーを含んでいます。

楽観的な更新

ReactのuseOptimisticフックを使用して、Server Actionの実行が完了する前にUIを楽観的に更新し、レスポンスを待つことなく更新することができます:

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

以下のようにフォーム要素にもイベントハンドラーを追加できます:

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> バウンダリによってキャッチされます。詳しくはエラーハンドリングを参照してください。

Good to know:

データの再検証

revalidatePath APIを使ってServer Actions内でNext.jsキャッシュを再検証することができます:

app/actions.ts
'use server'

import { revalidatePath } from 'next/cache'

export async function createPost() {
try {
// ...
} catch (error) {
// ...
}

revalidatePath('/posts')
}

または、特定のデータフェッチをキャッシュタグで無効化するには、revalidateTag APIを使用してください:

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

cookies APIを使って、Server Action内で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')
}

サーバーアクションからCookieを削除するための追加の例を参照してください。

セキュリティ

デフォルトでは、Server Actionが作成されてエクスポートされると、パブリックなHTTPエンドポイントが作成され、 それに伴うセキュリティ仮説と認可チェックが組み込まれます。つまり、サーバーのアクションまたはユーティリティ関数があなたのコードの他の場所でインポートされていなくても、それはまだパブリックにアクセス可能です。

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

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

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
// は自動でビルド中にこのコードを削除し、パブリックなエンドポイントを
// 作成しません。
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}>Publish</button>
</form>
);
}

クロージャは特にレンダリング時点のデータのスナップショット(例えば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メソッドを使用し、これがそれらを呼び出せる唯一のHTTPメソッドです。これによりモダンなブラウザでCSRFの脆弱性のほとんどが防止され、特にSameSite Cookieがデフォルトで有効です。

追加の保護として、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についてさらに詳しく学びましょう。

追加資料

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