server-actions について
server-actions は、データ変更やフォーム処理などに使う非同期サーバー関数、Next.js Server Actions の実装を案内するリファレンススキルです。フォーム状態、検証、キャッシュ再検証、リダイレクト、楽観的更新、安全対策を扱います。
主な機能
- 01コンポーネント内または別ファイルでアクションを定義
- 02フォーム処理と検証
- 03フォーム状態とエラーの管理
- 04パスまたはキャッシュタグの再検証
- 05リダイレクトと楽観的更新
- 06入力と認可の安全対策を確認
活用シーン
- 01サーバー側のフォーム送信を構築
- 02検証を伴うデータ変更を実装
- 03変更後にキャッシュ済みページを更新
- 04読み込み状態と楽観的 UI 更新を追加
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ davepoon/ buildwithclaude --skill server-actionsインストール後、Claude Code で/server-actionsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
examples/mutation-patterns.mdreferences/form-handling.mdrevalidation.md詳細
Next.js Server Actions
概要
Server Actions は、サーバー上で実行される非同期関数です。Client Components と Server Components から呼び出して、データの変更、フォームの送信、その他のサーバー側の操作を実行できます。
Server Actions の定義
Server Components 内で定義する
非同期関数内で 'use server' ディレクティブを使用します。
// app/page.tsx (Server Component)
export default function Page() {
async function createPost(formData: FormData) {
'use server'
const title = formData.get('title') as string
await db.post.create({ data: { title } })
}
return (
<form action={createPost}>
<input name="title" />
<button type="submit">Create</button>
</form>
)
}
別ファイルで定義する
ファイル全体に 'use server' を指定します。
// app/actions.ts
'use server'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
await db.post.create({ data: { title } })
}
export async function deletePost(id: string) {
await db.post.delete({ where: { id } })
}
フォームの処理
基本的なフォーム
// app/actions.ts
'use server'
export async function submitContact(formData: FormData) {
const name = formData.get('name') as string
const email = formData.get('email') as string
const message = formData.get('message') as string
await db.contact.create({
data: { name, email, message }
})
}
// app/contact/page.tsx
import { submitContact } from '@/app/actions'
export default function ContactPage() {
return (
<form action={submitContact}>
<input name="name" required />
<input name="email" type="email" required />
<textarea name="message" required />
<button type="submit">Send</button>
</form>
)
}
バリデーションを使用する場合(Zod)
// app/actions.ts
'use server'
import { z } from 'zod'
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
})
export async function signup(formData: FormData) {
const parsed = schema.safeParse({
email: formData.get('email'),
password: formData.get('password'),
})
if (!parsed.success) {
return { error: parsed.error.flatten() }
}
await createUser(parsed.data)
return { success: true }
}
useFormState Hook
フォームの状態とエラーを処理します。
// app/signup/page.tsx
'use client'
import { useFormState } from 'react-dom'
import { signup } from '@/app/actions'
const initialState = {
error: null,
success: false,
}
export default function SignupPage() {
const [state, formAction] = useFormState(signup, initialState)
return (
<form action={formAction}>
<input name="email" type="email" />
<input name="password" type="password" />
{state.error && (
<p className="text-red-500">{state.error}</p>
)}
<button type="submit">Sign Up</button>
</form>
)
}
// app/actions.ts
'use server'
export async function signup(prevState: any, formData: FormData) {
const email = formData.get('email') as string
if (!email.includes('@')) {
return { error: 'Invalid email', success: false }
}
await createUser({ email })
return { error: null, success: true }
}
useFormStatus Hook
送信中の読み込み状態を表示します。
// components/submit-button.tsx
'use client'
import { useFormStatus } from 'react-dom'
export function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
)
}
// Usage in form
import { SubmitButton } from '@/components/submit-button'
export default function Form() {
return (
<form action={submitAction}>
<input name="title" />
<SubmitButton />
</form>
)
}
再検証
revalidatePath
特定のパスを再検証します。
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData) {
await db.post.create({ data: { ... } })
// Revalidate the posts list page
revalidatePath('/posts')
// Revalidate a dynamic route
revalidatePath('/posts/[slug]', 'page')
// Revalidate all paths under /posts
revalidatePath('/posts', 'layout')
}
revalidateTag
キャッシュタグを使って再検証します。
// Fetching with tags
const posts = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] }
})
// Server Action
'use server'
import { revalidateTag } from 'next/cache'
export async function createPost(formData: FormData) {
await db.post.create({ data: { ... } })
revalidateTag('posts')
}
アクション後のリダイレクト
'use server'
import { redirect } from 'next/navigation'
export async function createPost(formData: FormData) {
const post = await db.post.create({ data: { ... } })
// Redirect to the new post
redirect(`/posts/${post.slug}`)
}
楽観的な更新
アクションの完了を待つ間、UI をすぐに更新します。
'use client'
import { useOptimistic } from 'react'
import { addTodo } from '@/app/actions'
export function TodoList({ todos }: { todos: Todo[] }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo: string) => [
...state,
{ id: 'temp', title: newTodo, completed: false }
]
)
async function handleSubmit(formData: FormData) {
const title = formData.get('title') as string
addOptimisticTodo(title) // Update UI immediately
await addTodo(formData) // Server action
}
return (
<>
<form action={handleSubmit}>
<input name="title" />
<button>Add</button>
</form>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</>
)
}
フォーム以外での使用
Server Actions をプログラムから呼び出します。
'use client'
import { deletePost } from '@/app/actions'
export function DeleteButton({ id }: { id: string }) {
return (
<button onClick={() => deletePost(id)}>
Delete
</button>
)
}
エラー処理
'use server'
export async function createPost(formData: FormData) {
try {
await db.post.create({ data: { ... } })
return { success: true }
} catch (error) {
if (error instanceof PrismaClientKnownRequestError) {
if (error.code === 'P2002') {
return { error: 'A post with this title already exists' }
}
}
return { error: 'Failed to create post' }
}
}
セキュリティに関する考慮事項
- 常に入力を検証する - クライアントのデータを決して信頼しない
- 認証を確認する - ユーザーに権限があることを検証する
- CSRF 対策を使用する - Server Actions に組み込まれています
- 出力をサニタイズする - XSS 攻撃を防止する
'use server'
import { auth } from '@/lib/auth'
export async function deletePost(id: string) {
const session = await auth()
if (!session) {
throw new Error('Unauthorized')
}
const post = await db.post.findUnique({ where: { id } })
if (post.authorId !== session.user.id) {
throw new Error('Forbidden')
}
await db.post.delete({ where: { id } })
}
リソース
詳細なパターンについては、以下を参照してください。
references/form-handling.md- 高度なフォームパターンreferences/revalidation.md- キャッシュ再検証の戦略examples/mutation-patterns.md- 変更処理の完全な例
コメント
Markdown 対応