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

Server Actionsとデータの変更

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

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

規約

Server Actionは、Reactの"use server"ディレクティブを使用して定義できます。このディレクティブをasync関数の先頭に配置して、その関数を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 ComponentとServer Componentの両方で再利用できるServer Actionとしてマークされます:

app/actions.ts
'use server'

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

import { create } from './actions'

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

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

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

<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という名前のpropsやActionで終わるpropsは、Server Actionsを受け取ると仮定されます。 これはあくまでヒューリスティックであり、TypeScriptプラグインは実際にServer Actionを受け取るか通常の関数を受け取るかを知りません。 ランタイムの型チェックは、誤ってClient Componentに関数を渡さないようにすることを保証します。

挙動

  • Server Actionsは、<form>要素action属性を使用して呼び出すことができます:
    • Server Componentsはデフォルトでプログレッシブエンハンスメントをサポートしており、JavaScriptがまだ読み込まれていないか無効になっている場合でもフォームが送信されます。
    • Client Componentsでは、Server Actionsを呼び出すフォームは、JavaScriptがまだ読み込まれていない場合に送信をキューに入れ、クライアントのハイドレーションを優先します。
    • ハイドレーション後、フォーム送信時にブラウザはリフレッシュしません。
  • Server Actionsは<form>に限定されず、イベントハンドラ、useEffect、サードパーティライブラリ、<button>などの他のフォーム要素からも呼び出すことができます。
  • Server Actionsは、Next.jsのキャッシュと再検証アーキテクチャと統合されています。アクションが呼び出されると、Next.jsは更新されたUIと新しいデータを単一のサーバーラウンドトリップで返すことができます。
  • 背後では、アクションは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:

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

追加の引数を渡す

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

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

Good to know:

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

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

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

これは、フォーム内で複数のServer Actionを呼び出したい場合に便利です。例えば、投稿の下書きを保存するための特定の<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のuseActionStateフックを使用してユーザーにメッセージを表示できます。

  • アクションをuseActionStateに渡すことで、アクションの関数シグネチャが変更され、新しいprevStateまたはinitialStateパラメータを最初の引数として受け取るようになります。
  • useActionStateは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')
}

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

app/ui/signup.tsx
'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フックを使用して、アクションが実行されている間にローディングインジケーターを表示することもできます。このフックを使用する場合、ローディングインジケーターをレンダリングするための別のコンポーネントを作成する必要があります。例えば、アクションが保留中のときにボタンを無効にするには:

app/ui/button.tsx
'use client'

import { useFormStatus } from 'react-dom'

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

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

次に、SubmitButtonコンポーネントをフォーム内にネストできます:

app/ui/signup.tsx
import { SubmitButton } from './button'
import { createUser } from '@/app/actions'

export function Signup() {
return (
<form action={createUser}>
{/* 他のフォーム要素 */}
<SubmitButton />
</form>
)
}

Good to know: React 19では、useFormStatusは返されるオブジェクトにデータ、メソッド、アクションなどの追加のキーを含みます。React 19を使用していない場合、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: 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>境界によってキャッチされます。詳細については、エラーハンドリングを参照してください。

Good to know:

データの再検証

Server Actions内でrevalidatePath APIを使用して、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の完了後にユーザーを別のルートにリダイレクトしたい場合は、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は暗号化された非決定的なIDを作成し、クライアントがServer Actionを参照して呼び出すことを可能にします。これらの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('You must be signed in to perform this action')
}

// ...
}

クロージャと暗号化

コンポーネント内で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('The version has changed since pressing publish');
}
...
}

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メソッドのみがそれらを呼び出すことができます。これにより、特にSameSite Cookieがデフォルトである現代のブラウザでのほとんどのCSRF脆弱性が防止されます。

追加の保護として、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ドキュメントを参照してください: