mcpField
S
server-actions

server-actions

github.com/davepoon開発3.7k197 回インストール今日更新
リポジトリ

server-actions について

server-actions は、データ変更やフォーム処理などに使う非同期サーバー関数、Next.js Server Actions の実装を案内するリファレンススキルです。フォーム状態、検証、キャッシュ再検証、リダイレクト、楽観的更新、安全対策を扱います。

主な機能

  1. 01コンポーネント内または別ファイルでアクションを定義
  2. 02フォーム処理と検証
  3. 03フォーム状態とエラーの管理
  4. 04パスまたはキャッシュタグの再検証
  5. 05リダイレクトと楽観的更新
  6. 06入力と認可の安全対策を確認

活用シーン

  1. 01サーバー側のフォーム送信を構築
  2. 02検証を伴うデータ変更を実装
  3. 03変更後にキャッシュ済みページを更新
  4. 04読み込み状態と楽観的 UI 更新を追加

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/davepoon/buildwithclaude --skill server-actions

インストール後、Claude Code で/server-actionsを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

examples/
mutation-patterns.md
references/
form-handling.md
revalidation.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' }
  }
}

セキュリティに関する考慮事項

  1. 常に入力を検証する - クライアントのデータを決して信頼しない
  2. 認証を確認する - ユーザーに権限があることを検証する
  3. CSRF 対策を使用する - Server Actions に組み込まれています
  4. 出力をサニタイズする - 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 - 変更処理の完全な例

コメント