サーバーアクションを使用したフォーム作成方法
Reactサーバーアクションは、サーバー上で実行されるサーバー関数です。サーバーコンポーネントとクライアントコンポーネントの両方で呼び出され、フォーム送信を処理できます。このガイドでは、Next.jsでサーバーアクションを使用してフォームを作成する方法を説明します。
動作原理
ReactはHTMLの<form>
要素を拡張し、action
属性でサーバーアクションを呼び出せるようにしています。
フォームで使用すると、関数は自動的にFormData
オブジェクトを受け取ります。その後、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'),
}
// データを更新
// キャッシュを再検証
}
return <form action={createInvoice}>...</form>
}
export default function Page() {
async function createInvoice(formData) {
'use server'
const rawFormData = {
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
}
// データを更新
// キャッシュを再検証
}
return <form action={createInvoice}>...</form>
}
補足: 複数のフィールドを持つフォームを扱う場合、JavaScriptの
Object.fromEntries()
とentries()
メソッドを使用できます。例:const rawFormData = Object.fromEntries(formData)
。
追加引数の渡し方
フォームフィールド以外で追加の引数をサーバー関数に渡すには、JavaScriptのbind
メソッドを使用します。例えば、userId
引数をupdateUser
サーバー関数に渡す場合:
'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">ユーザー名を更新</button>
</form>
)
}
'use client'
import { updateUser } from './actions'
export function UserProfile({ userId }) {
const updateUserWithId = updateUser.bind(null, userId)
return (
<form action={updateUserWithId}>
<input type="text" name="name" />
<button type="submit">ユーザー名を更新</button>
</form>
)
}
サーバー関数はuserId
を追加引数として受け取ります:
'use server'
export async function updateUser(userId: string, formData: FormData) {}
'use server'
export async function updateUser(userId, formData) {}
補足:
- 代替方法として、フォーム内に隠し入力フィールドとして引数を渡すこともできます(例:
<input type="hidden" name="userId" value={userId} />
)。ただし、値はレンダリングされたHTMLの一部となり、エンコードされません。bind
はサーバーコンポーネントとクライアントコンポーネントの両方で動作し、プログレッシブエンハンスメントをサポートします。
フォーム検証
フォームはクライアント側またはサーバー側で検証できます。
- クライアント側検証には、基本的な検証のために
required
やtype="email"
などのHTML属性を使用できます。 - サーバー側検証には、zodなどのライブラリを使用してフォームフィールドを検証できます。例:
'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,
}
}
// データを更新
}
'use server'
import { z } from 'zod'
const schema = z.object({
email: z.string({
invalid_type_error: '無効なメールアドレス',
}),
})
export default async function createsUser(formData) {
const validatedFields = schema.safeParse({
email: formData.get('email'),
})
// フォームデータが無効な場合は早期リターン
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
// データを更新
}
検証エラー
検証エラーやメッセージを表示するには、<form>
を定義するコンポーネントをクライアントコンポーネントに変更し、ReactのuseActionState
を使用します。
useActionState
を使用すると、サーバー関数のシグネチャが変更され、最初の引数として新しいprevState
またはinitialState
パラメータを受け取るようになります。
'use server'
import { z } from 'zod'
export async function createUser(initialState: any, formData: FormData) {
const validatedFields = schema.safeParse({
email: formData.get('email'),
})
// ...
}
'use server'
import { z } from 'zod'
// ...
export async function createUser(initialState, formData) {
const validatedFields = schema.safeParse({
email: formData.get('email'),
})
// ...
}
その後、state
オブジェクトに基づいて条件付きでエラーメッセージをレンダリングできます。
'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">メールアドレス</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite">{state?.message}</p>
<button disabled={pending}>登録</button>
</form>
)
}
'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">メールアドレス</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite">{state?.message}</p>
<button disabled={pending}>登録</button>
</form>
)
}
保留状態
useActionState
フックは、アクション実行中にローディングインジケーターを表示したり送信ボタンを無効にしたりするために使用できるpending
ブール値を公開します。
'use client'
import { useActionState } from 'react'
import { createUser } from '@/app/actions'
export function Signup() {
const [state, formAction, pending] = useActionState(createUser, initialState)
return (
<form action={formAction}>
{/* 他のフォーム要素 */}
<button disabled={pending}>登録</button>
</form>
)
}
'use client'
import { useActionState } from 'react'
import { createUser } from '@/app/actions'
export function Signup() {
const [state, formAction, pending] = useActionState(createUser, initialState)
return (
<form action={formAction}>
{/* 他のフォーム要素 */}
<button disabled={pending}>登録</button>
</form>
)
}
あるいは、useFormStatus
フックを使用して、アクション実行中にローディングインジケーターを表示することもできます。このフックを使用する場合、ローディングインジケーターをレンダリングするための別のコンポーネントを作成する必要があります。例えば、アクションが保留中にボタンを無効にするには:
その後、SubmitButton
コンポーネントをフォーム内にネストできます:
import { SubmitButton } from './button'
import { createUser } from '@/app/actions'
export function Signup() {
return (
<form action={createUser}>
{/* 他のフォーム要素 */}
<SubmitButton />
</form>
)
}
import { SubmitButton } from './button'
import { createUser } from '@/app/actions'
export function Signup() {
return (
<form action={createUser}>
{/* 他のフォーム要素 */}
<SubmitButton />
</form>
)
}
補足: React 19では、
useFormStatus
は返されるオブジェクトにdata、method、actionなどの追加キーを含みます。React 19を使用していない場合、pending
キーのみが利用可能です。
楽観的更新
ReactのuseOptimistic
フックを使用して、サーバー関数の実行が完了するのを待たずにUIを楽観的に更新できます:
'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">送信</button>
</form>
</div>
)
}
'use client'
import { useOptimistic } from 'react'
import { send } from './actions'
export function Thread({ messages }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage) => [...state, { message: newMessage }]
)
const formAction = async (formData) => {
const message = formData.get('message')
addOptimisticMessage(message)
await send(message)
}
return (
<div>
{optimisticMessages.map((m) => (
<div>{m.message}</div>
))}
<form action={formAction}>
<input type="text" name="message" />
<button type="submit">送信</button>
</form>
</div>
)
}
ネストされたフォーム要素
<button>
、<input type="submit">
、<input type="image">
など、<form>
内にネストされた要素でサーバーアクションを呼び出せます。これらの要素はformAction
プロップまたはイベントハンドラーを受け入れます。
これは、フォーム内で複数のサーバーアクションを呼び出したい場合に便利です。例えば、投稿を公開するだけでなく下書きとして保存するための特定の<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>
)
}
'use client'
export function Entry() {
const handleKeyDown = (e) => {
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>
祖先の送信がトリガーされ、サーバー関数が呼び出されます。