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"
ディレクティブを追加します:
// 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としてマークされます:
'use server'
export async function create() {
// ...
}
import { create } from '@/app/actions'
export function Button() {
return (
// ...
)
}
また、Server ActionをpropとしてClient Componentに渡すこともできます:
<ClientComponent updateItem={updateItem} />
'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>
要素を拡張し、action
propを使用してServer Actionsを呼び出すことを可能にします。
フォーム内で呼び出されると、アクションは自動的に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>
}
Good to know:
- 例:Form with Loading & Error States
- 多くのフィールドを持つフォームを操作する場合は、JavaScriptの
Object.fromEntries()
メソッドとentries()
メソッドの使用を検討すると良いでしょう。例えば、次のようになります:const rawFormData = Object.fromEntries(formData.entries())
- 詳細については、Reactの
<form>
のドキュメントを参照してください。
追加の引数の渡し方
JavaScriptのbind
メソッドを使用して、Server Actionに追加の引数を渡すことができます。
'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
を受け取ります:
'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内で使用する必要があります。
'use client'
import { useFormStatus } from 'react-dom'
export function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" aria-disabled={pending}>
Add
</button>
)
}
<SubmitButton />
は、任意のフォーム内にネストできます:
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>
)
}
サーバーサイドのバリデーションとエラーハンドリング
基本的なクライアントサイドのフォームバリデーションには、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のuseFormState
フックを使用してユーザーにメッセージを表示できます。
useFormState
にアクションを渡すことで、アクションの関数シグネチャが変更され、最初の引数として新しいprevState
またはinitialState
パラメータを受け取ります。useFormState
はReactのフックであるため、Client Component内で使用する必要があります。
'use server'
export async function createUser(prevState: any, formData: FormData) {
// ...
return {
message: 'Please enter a valid email',
}
}
その後、useFormState
フックにアクションを渡し、返されたstate
を使用してエラーメッセージを表示できます。
'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
フックを使用することができます:
'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
イベントを受信できます:
'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のカウントを増やす場合:
'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を更新したり、保留中の状態を表示するために、useOptimistic
やuseTransition
などの他のReactのAPIを使用することをお勧めします。
フォーム要素には、例えばフォームフィールドの変更時に保存するための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>
)
}
このような場合、複数のイベントが素早く連続して発生する可能性があるため、不要なServer Actionの呼び出しを防ぐためにデバウンシングをお勧めします。
useEffect
ReactのuseEffect
フックを使用して、コンポーネントがマウントされたときや依存関係が変更されたときにServer Actionを呼び出すことができます。これは、グローバルなイベントに依存する変更や自動的にトリガーする必要がある変更に役立ちます。例えば、アプリケーションのショートカットのためのonKeyDown
、無限スクロールのためのIntersection Observerフック、またはコンポーネントがマウントされたときにビュ ーカウントを更新する場合などが挙げられます:
'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は新しいアイテムを作成する際のエラーをメッセージで返すように処理するかもしれません:
'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:
- エラーを投げるだけでなく、
useFormState
で処理するためにオブジェクトを返すこともできます。サーバーサイドのバリデーションとエラーハンドリングを参照してください。
データの再検証
Server Actions内で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')
}
リダイレクト
Server Actionの完了後にユーザーを別のルートにリダイレクトしたい場合、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') // Update cached posts
redirect(`/post/${id}`) // Navigate to the new post page
}
クッキー
Server Action内でcookies
APIを使用して、クッキーをget
(取得)、set
(設定)、delete
(削除)することができます。
'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内でクッキーを削除する追加の例については、関連する例をご覧ください。