コンテンツへスキップ
アプリケーションの構築データの取得サーバーアクションとミューテーション

サーバーアクションとミューテーション

サーバーアクションは、サーバー上で実行される**非同期関数**です。Next.jsアプリケーションでフォームの送信とデータのミューテーションを処理するために、サーバーコンポーネントとクライアントコンポーネントの両方で呼び出すことができます。

🎥動画: サーバーアクションによるミューテーションの詳細はこちら → YouTube (10分).

規約

サーバーアクションは、Reactの"use server"ディレクティブを使用して定義できます。このディレクティブを`async`関数の先頭に配置して関数をサーバーアクションとしてマークするか、個別のファイルの先頭に配置してそのファイルのすべてのエクスポートをサーバーアクションとしてマークします。

サーバーコンポーネント

サーバーコンポーネントでは、インライン関数レベルまたはモジュールレベルの `"use server"` ディレクティブを使用できます。サーバーアクションをインラインするには、関数の先頭に `"use server"` を追加します。

app/page.tsx
export default function Page() {
  // Server Action
  async function create() {
    'use server'
    // Mutate data
  }
 
  return '...'
}

クライアントコンポーネント

クライアントコンポーネントでサーバーアクションを呼び出すには、新しいファイルを作成し、その先頭に `"use server"` ディレクティブを追加します。ファイル内のすべてのエクスポートされた関数は、クライアントコンポーネントとサーバーコンポーネントの両方で再利用できるサーバーアクションとしてマークされます。

app/actions.ts
'use server'
 
export async function create() {}
app/ui/button.tsx
'use client'
 
import { create } from '@/app/actions'
 
export function Button() {
  return <button onClick={() => create()}>Create</button>
}

プロパティとしてアクションを渡す

サーバーアクションをプロップとしてクライアントコンポーネントに渡すこともできます。

<ClientComponent updateItemAction={updateItem} />
app/client-component.tsx
'use client'
 
export default function ClientComponent({
  updateItemAction,
}: {
  updateItemAction: (formData: FormData) => void
}) {
  return <form action={updateItemAction}>{/* ... */}</form>
}

通常、Next.js TypeScriptプラグインは、`client-component.tsx`内の`updateItemAction`にフラグを立てます。これは、一般的にクライアントとサーバー間の境界を越えてシリアル化できない関数だからです。しかし、`action`という名前のプロップ、または`Action`で終わるプロップは、サーバーアクションを受け取るとみなされます。これはヒューリスティックな方法に過ぎません。なぜなら、TypeScriptプラグインは実際にはサーバーアクションと通常の関数のどちらを受け取っているかを知らないからです。ランタイムの型チェックによって、誤って関数をクライアントコンポーネントに渡すことがないようにします。

動作

  • サーバーアクションは、`<form>要素`の`action`属性を使用して呼び出すことができます。
    • サーバーコンポーネントはデフォルトでプログレッシブエンハンスメントをサポートしており、JavaScriptがまだ読み込まれていない場合や無効になっている場合でも、フォームが送信されます。
    • クライアントコンポーネントでは、サーバーアクションを呼び出すフォームは、JavaScriptがまだ読み込まれていない場合、送信をキューに登録し、クライアントのハイドレーションを優先します。
    • ハイドレーション後、ブラウザはフォーム送信時に更新されません。
  • サーバーアクションは`
    `に限定されず、イベントハンドラ、`useEffect`、サードパーティライブラリ、および`
  • サーバーアクションは、Next.jsのキャッシュと再検証アーキテクチャと統合されています。アクションが呼び出されると、Next.jsは更新されたUIと新しいデータを単一のサーバーラウンドトリップで返すことができます。
  • 内部的には、アクションは`POST`メソッドを使用し、このHTTPメソッドのみがアクションを呼び出すことができます。
  • サーバーアクションの引数と戻り値は、Reactによってシリアル化可能でなければなりません。シリアル化可能な引数と値のリストについては、Reactのドキュメントを参照してください。
  • サーバーアクションは関数です。つまり、アプリケーション内のどこでも再利用できます。
  • サーバーアクションは、使用されているページまたはレイアウトからランタイムを継承します。
  • サーバーアクションは、使用されているページまたはレイアウトからルートセグメント設定を継承します(`maxDuration`などのフィールドを含む)。

フォーム

ReactはHTMLの<form>要素を拡張して、`action`プロップを使用してサーバーアクションを呼び出すことができます。

フォーム内で呼び出された場合、アクションは自動的にFormDataオブジェクトを受け取ります。フィールドを管理するためにReactの`useState`を使用する必要はありません。代わりに、ネイティブのFormDataメソッドを使用してデータを取り出すことができます。

app/invoices/page.tsx
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>
}

知っておくと良いこと

追加の引数の渡す

JavaScriptの`bind`メソッドを使用して、サーバーアクションに追加の引数を渡すことができます。

app/client-component.tsx
'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>
  )
}

サーバーアクションは、フォームデータに加えて`userId`引数を受け取ります。

app/actions.js
'use server'
 
export async function updateUser(userId, formData) {}

知っておくと良いこと:

  • 別の方法として、フォームに非表示の入力フィールドとして引数を渡すことができます(例:`<input type="hidden" name="userId" value={userId} />`)。ただし、値はレンダリングされたHTMLの一部になり、エンコードされません。
  • `.bind`は、サーバーコンポーネントとクライアントコンポーネントの両方で機能します。また、プログレッシブエンハンスメントもサポートしています。

ネストされたフォーム要素

``内にネストされた要素(`

'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>祖先要素の送信をトリガーし、サーバーアクションを呼び出します。

サーバーサイドフォーム検証

基本的なクライアントサイドのフォーム検証には、requiredtype="email"などのHTML属性を使用できます。

より高度なサーバーサイド検証には、zodのようなライブラリを使用して、データ変更前にフォームフィールドを検証できます。

app/actions.ts
'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フックを使用してユーザーにメッセージを表示できます。

app/actions.ts
'use server'
 
import { redirect } from 'next/navigation'
 
export async function createUser(prevState: any, formData: FormData) {
  const res = await fetch('https://...')
  const json = await res.json()
 
  if (!res.ok) {
    return { message: 'Please enter a valid email' }
  }
 
  redirect('/dashboard')
}

次に、アクションをuseFormStateフックに渡し、返されたstateを使用してエラーメッセージを表示できます。

app/ui/signup.tsx
'use client'
 
import { useFormState } from 'react'
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">{state?.message}</p>
      <button>Sign up</button>
    </form>
  )
}

知っておくと良いこと

保留中の状態

useFormStatusフックは、アクションの実行中にローディングインジケーターを表示するために使用できるpendingブール値を公開します。

app/submit-button.tsx
'use client'
 
import { useFormStatus } from 'react-dom'
 
export function SubmitButton() {
  const { pending } = useFormStatus()
 
  return (
    <button disabled={pending} type="submit">
      Sign Up
    </button>
  )
}

知っておくと良いこと

楽観的更新

ReactのuseOptimisticフックを使用して、サーバーアクションの実行が完了するのを待つのではなく、UIを楽観的に更新できます。

app/page.tsx
'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) => {
    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">Send</button>
      </form>
    </div>
  )
}

イベントハンドラー

<form>要素内でサーバーアクションを使用するのが一般的ですが、onClickなどのイベントハンドラーでも呼び出すことができます。たとえば、いいね数を増やす場合などです。

app/like-button.tsx
'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>
    </>
  )
}

フォーム要素にイベントハンドラーを追加することもできます。たとえば、フォームフィールドをonChangeで保存する場合などです。

app/ui/edit-post.tsx
'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>
  )
}

このように、複数のイベントが短時間に連続して発生する可能性がある場合は、不要なサーバーアクションの呼び出しを防ぐために、デバウンスをお勧めします。

useEffect

ReactのuseEffectフックを使用して、コンポーネントのマウント時または依存関係の変更時にサーバーアクションを呼び出すことができます。これは、グローバルイベントに依存する、または自動的にトリガーする必要がある変更に役立ちます。たとえば、アプリショートカットのonKeyDown、無限スクロールのIntersection Observerフック、またはビューカウントを更新するためのコンポーネントのマウント時などです。

app/view-count.tsx
'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を使用することをお勧めします。

たとえば、サーバーアクションは、新しいアイテムの作成からのエラーをメッセージを返すことで処理できます。

app/actions.ts
'use server'
 
export async function createTodo(prevState: any, formData: FormData) {
  try {
    // Mutate data
  } catch (e) {
    throw new Error('Failed to create task')
  }
}

知っておくと良いこと

データの再検証

Next.jsキャッシュは、revalidatePath APIを使用してサーバーアクション内で再検証できます。

app/actions.ts
'use server'
 
import { revalidatePath } from 'next/cache'
 
export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }
 
  revalidatePath('/posts')
}

または、revalidateTagを使用してキャッシュタグ付きの特定のデータフェッチを無効化します。

app/actions.ts
'use server'
 
import { revalidateTag } from 'next/cache'
 
export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }
 
  revalidateTag('posts')
}

リダイレクト

サーバーアクションの完了後にユーザーを別のルートにリダイレクトする場合は、redirect APIを使用できます。redirecttry/catchブロックの外で呼び出す必要があります。

app/actions.ts
'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
}

クッキー

cookies APIを使用して、サーバーアクション内でクッキーを取得、設定、削除できます。

app/actions.ts
'use server'
 
import { cookies } from 'next/headers'
 
export async function exampleAction() {
  const cookieStore = await cookies()
 
  // Get cookie
  cookieStore.get('name')?.value
 
  // Set cookie
  cookieStore.set('name', 'Delba')
 
  // Delete cookie
  cookieStore.delete('name')
}

サーバーアクションからクッキーを削除する例については、追加の例を参照してください。

セキュリティ

デフォルトでは、サーバーアクションが作成およびエクスポートされると、公開HTTPエンドポイントが作成され、同じセキュリティの仮定と承認チェックで処理する必要があります。つまり、サーバーアクションまたはユーティリティ関数がコードの他の場所でインポートされていない場合でも、公開アクセス可能です。

セキュリティを向上させるために、Next.jsには次の組み込み機能があります。

知っておくと良いこと:

IDはコンパイル時に作成され、最大14日間キャッシュされます。新しいビルドが開始された場合、またはビルドキャッシュが無効化された場合、再生成されます。このセキュリティ強化により、認証レイヤーがない場合のリスクが軽減されます。ただし、サーバーアクションは引き続き公開HTTPエンドポイントとして扱う必要があります。

// app/actions.js
'use server'
 
// This action **is** used in our application, so Next.js
// will create a secure ID to allow the client to reference
// and call the Server Action.
export async function updateUserAction(formData) {}
 
// This action **is not** used in our application, so Next.js
// will automatically remove this code during `next build`
// and will not create a public endpoint.
export async function deleteUserAction(formData) {}

認証と認可

ユーザーがアクションを実行する権限を持っていることを確認する必要があります。例:

app/actions.ts
'use server'
 
import { auth } from './lib'
 
export function addItem() {
  const { user } = auth()
  if (!user) {
    throw new Error('You must be signed in to perform this action')
  }
 
  // ...
}

クロージャと暗号化

コンポーネント内でサーバーアクションを定義すると、クロージャが作成され、アクションは外部関数のスコープにアクセスできます。例えば、publishアクションはpublishVersion変数にアクセスできます。

app/page.tsx
export default async function Page() {
  const publishVersion = await getLatestVersion();
 
  async function publish() {
    "use server";
    if (publishVersion !== await getLatestVersion()) {
      throw new Error('The version has changed since pressing publish');
    }
    ...
  }
 
  return (
    <form>
      <button formAction={publish}>Publish</button>
    </form>
  );
}

クロージャは、レンダリング時にデータのスナップショット(例:publishVersion)を取得し、アクションが呼び出されたときに後で使用できるようにする必要がある場合に便利です。

ただし、これを実現するには、アクションが呼び出されたときに、取得された変数がクライアントに送信され、サーバーに戻されます。機密データがクライアントに公開されるのを防ぐため、Next.jsはクロージャされた変数を自動的に暗号化します。Next.jsアプリケーションがビルドされるたびに、各アクションに対して新しい秘密鍵が生成されます。これは、アクションを特定のビルドに対してのみ呼び出すことができることを意味します。

重要事項: クライアントへの機密値の公開を防ぐために、暗号化だけに依存することはお勧めしません。代わりに、React taint APIを使用して、特定のデータがクライアントに送信されないように事前に防止する必要があります。

暗号化キーのオーバーライド(上級者向け)

複数のサーバーにNext.jsアプリケーションをセルフホスティングする場合、各サーバーインスタンスで異なる暗号化キーが使用される可能性があり、不整合が発生する可能性があります。

これを軽減するために、process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY環境変数を使用して暗号化キーをオーバーライドできます。この変数を指定することで、暗号化キーがビルド間で永続化され、すべてのサーバーインスタンスで同じキーが使用されるようになります。

これは、複数のデプロイメントにわたる一貫した暗号化動作がアプリケーションにとって重要となる高度なユースケースです。キーローテーションや署名などの標準的なセキュリティプラクティスを検討する必要があります。

重要事項: VercelにデプロイされたNext.jsアプリケーションでは、これが自動的に処理されます。

許可されたオリジン(上級者向け)

サーバーアクションは<form>要素で呼び出すことができるため、CSRF攻撃に対して脆弱になります。

内部的には、サーバーアクションはPOSTメソッドを使用し、このHTTPメソッドのみがそれらを呼び出すことができます。これは、特にSameSiteクッキーがデフォルトである最新のブラウザでは、ほとんどのCSRFの脆弱性を防ぎます。

追加の保護として、Next.jsのサーバーアクションはOriginヘッダーHostヘッダー(またはX-Forwarded-Host)を比較します。これらが一致しない場合、リクエストは中止されます。つまり、サーバーアクションは、それをホストするページと同じホストでのみ呼び出すことができます。

リバースプロキシまたは多層バックエンドアーキテクチャ(サーバーAPIが本番ドメインと異なる場合)を使用する大規模なアプリケーションでは、構成オプションserverActions.allowedOriginsを使用して、安全なオリジンのリストを指定することをお勧めします。このオプションは、文字列の配列を受け入れます。

next.config.js
/** @type {import('next').NextConfig} */
module.exports = {
  experimental: {
    serverActions: {
      allowedOrigins: ['my-proxy.com', '*.my-proxy.com'],
    },
  },
}

サーバーアクションのセキュリティに関する詳細について学習します。

追加のリソース

詳細については、次のReactドキュメントを参照してください。

次のステップ

Next.jsでのサーバーアクションの構成方法について学習します。