Next.js 15 Server Actions が動かない・エラーになる
Server Actions は Next.js 14 で安定版となり、フォーム送信やデータ更新の定番パターンになりました。しかし Next.js 15 + React 19 への移行で 破壊的変更が入っており、以前の記事の通りに書いても動かないケースが増えています。
よく見るエラー:
Error: Functions cannot be passed directly to Client Components unless
you explicitly expose it by marking it with "use server".
Error: Only plain objects, and a few built-ins, can be passed to Server Actions.
Classes or null prototypes are not supported.
Warning: useFormState is deprecated. Use useActionState instead.
順番に原因と対処法を解説します。
原因1:‘use server’ ディレクティブの位置が間違っている
コンポーネントファイルの先頭に 'use server' を書くと、そのファイル全体が Server Component 扱いになり、クライアント側の処理が動かなくなります。
// ❌ NG: コンポーネントファイルのトップに書いている
'use server'
export default function ContactForm() {
// このファイル全体がサーバー扱いになってしまう
const handleSubmit = async (formData: FormData) => {
// ...
}
return <form action={handleSubmit}>...</form>
}
正しい書き方1:Server Actions を別ファイルに切り出す
// app/actions/contact.ts
'use server' // ← このファイル全体が Server Actions になる
export async function submitContact(formData: FormData) {
const email = formData.get('email') as string
// DB保存・メール送信など
return { success: true }
}
// app/contact/page.tsx(Server Component または Client Component)
import { submitContact } from '../actions/contact'
export default function ContactForm() {
return <form action={submitContact}>...</form>
}
正しい書き方2:インライン関数の先頭に書く
// app/contact/page.tsx(Server Component 内のみ有効)
export default function ContactPage() {
async function handleSubmit(formData: FormData) {
'use server' // ← 関数スコープの先頭に書く
const email = formData.get('email') as string
// ...
}
return <form action={handleSubmit}>...</form>
}
インラインの 'use server' は Server Component 内でしか使えません。Client Component('use client' を付けたファイル)でインライン Server Action は定義できないため、必ず別ファイルに切り出してください。
原因2:シリアライズできないデータを返している
Server Actions がクライアントに返せるのは JSON シリアライズ可能なデータのみです。Date、Map、Set、クラスインスタンスなどはそのまま返せません。
// ❌ NG: Date オブジェクトをそのまま返す(Prisma の場合によくある)
'use server'
export async function getUser(id: string) {
const user = await db.user.findUnique({ where: { id } })
return user // createdAt が Date 型の場合エラー
}
Error: Only plain objects, and a few built-ins, can be passed to Server Actions.
Classes or null prototypes are not supported.
対処法:Date を ISO 文字列に変換して返す
// ✅ OK: Date を ISO 文字列に変換
'use server'
export async function getUser(id: string) {
const user = await db.user.findUnique({ where: { id } })
if (!user) return null
return {
...user,
createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt.toISOString(),
}
}
また、引数も同様にシリアライズ可能な型のみ受け取れます。ファイルアップロードは File オブジェクトではなく FormData 経由で渡す必要があります。
原因3:useFormState が useActionState に変わった(React 19)
React 18 + Next.js 14 では react-dom の useFormState を使っていましたが、React 19 で useActionState に名前が変わりました。
// ❌ NG: React 19 では useFormState は非推奨(将来的に削除される)
import { useFormState } from 'react-dom'
export function ContactForm() {
const [state, formAction] = useFormState(submitContact, null)
// ...
}
対処法:useActionState に移行する
// ✅ OK: React 19 の useActionState を使う
'use client'
import { useActionState } from 'react' // react-dom ではなく react からインポート
export function ContactForm() {
const [state, formAction, isPending] = useActionState(submitContact, null)
return (
<form action={formAction}>
<input name="email" type="email" required />
<button type="submit" disabled={isPending}>
{isPending ? '送信中...' : '送信する'}
</button>
{state?.error && <p style={{ color: 'red' }}>{state.error}</p>}
{state?.success && <p>送信しました!</p>}
</form>
)
}
変更点のまとめ:
| 項目 | React 18(旧) | React 19(新) |
|---|---|---|
| 関数名 | useFormState |
useActionState |
| インポート元 | react-dom |
react |
| 戻り値 | [state, action] の2要素 |
[state, action, isPending] の3要素 |
React 19 では isPending が第3要素として返るようになったため、useFormStatus を別途使う必要がなくなりました。
原因4:Server Actions でエラーをそのまま throw している
Server Actions 内で throw new Error('...') すると、本番環境ではエラーメッセージが隠蔽されます。Next.js が「An error occurred in the Server Components render.」のような汎用メッセージに置き換えるため、ユーザーにも開発者にも何が起きたか分かりません。
// ❌ NG: エラーをそのまま throw する
'use server'
export async function submitContact(formData: FormData) {
const email = formData.get('email') as string
if (!email) {
throw new Error('メールアドレスが必要です') // 本番では詳細が見えない
}
}
対処法:バリデーションエラーは戻り値として返す
// ✅ OK: エラーを戻り値として返す
'use server'
type ActionResult = {
success: boolean
error?: string
}
export async function submitContact(
prevState: ActionResult | null,
formData: FormData
): Promise<ActionResult> {
const email = formData.get('email') as string
if (!email) {
return { success: false, error: 'メールアドレスが必要です' }
}
try {
await sendEmail(email)
return { success: true }
} catch {
return { success: false, error: 'メール送信に失敗しました。時間をおいて再試行してください。' }
}
}
useActionState と組み合わせると、戻り値の error を直接フォームに表示できます。予期しない例外(DB 接続エラーなど)は throw してもよいですが、ユーザー向けのバリデーションエラーは必ず戻り値に含めてください。
原因5:redirect() が Server Action 内でエラーになる
redirect() を try/catch で囲むと動きません。redirect() は内部的に特殊な例外をスローする仕組みになっており、catch に捕まると NEXT_REDIRECT エラーとして扱われてしまいます。
// ❌ NG: try/catch で redirect を囲んでいる
'use server'
import { redirect } from 'next/navigation'
export async function loginAction(formData: FormData) {
try {
await authenticate(formData)
redirect('/dashboard') // ← catch に捕まってしまう
} catch {
return { error: 'ログインに失敗しました' }
}
}
Error: NEXT_REDIRECT ← redirect が例外として catch される
対処法1:isRedirectError で redirect 例外を再スロー
// ✅ OK: redirect エラーは再スローする
'use server'
import { redirect } from 'next/navigation'
import { isRedirectError } from 'next/dist/client/components/redirect'
export async function loginAction(formData: FormData) {
try {
await authenticate(formData)
redirect('/dashboard')
} catch (e) {
if (isRedirectError(e)) throw e // redirect は再スロー
return { error: 'ログインに失敗しました' }
}
}
対処法2:redirect() を try/catch の外に出す
// ✅ OK: 成功フラグで分岐し、try/catch の外でリダイレクト
'use server'
import { redirect } from 'next/navigation'
export async function loginAction(formData: FormData) {
let succeeded = false
try {
await authenticate(formData)
succeeded = true
} catch {
return { error: 'ログインに失敗しました' }
}
if (succeeded) {
redirect('/dashboard') // try/catch の外なので問題なし
}
}
まとめ
| 症状 | 原因 | 対処法 |
|---|---|---|
| Functions cannot be passed to Client Components | 'use server' の位置が間違い |
関数の先頭または別ファイルに 'use server' を移動 |
| Classes or null prototypes are not supported | Date/Map などをそのまま返している | ISO 文字列など JSON 変換可能な型に変換 |
| useFormState is not a function | React 19 で API が変わった | useActionState(react からインポート)に移行 |
| 本番でエラー詳細が見えない | エラーを throw している | バリデーションエラーは戻り値として返す |
| NEXT_REDIRECT エラー | redirect を try/catch で囲んでいる | isRedirectError で再スロー or try/catch の外に移動 |
Next.js 15 + React 19 への移行では、特に useFormState → useActionState の変更と redirect() の扱いが詰まりポイントです。公式ドキュメントも頻繁に更新されているため、古い記事を参考にする際は Next.js のバージョンを必ず確認するようにしてください。
