本文へ移動
verified onnext@16.3.3react@19
最終検証日 2026-08-27
Next.js道場

Server Function の呼び方

フォームの action に渡すのが基本の形です。

#呼び方は3つある

Server Function の渡し方は、境界に穴を開けるところまでの話でした。ここでは、開けた穴をクライアントからどう呼ぶかを扱います。

3つの呼び方。yes は「自分で書かなくても手に入る」という意味
書き方送信中が分かる結果を受け取れる
form の action に渡すできないできない
useActionState に渡すできるできる
イベントハンドラーの中で呼ぶできないできる

一番素直なのは、フォームの action に渡す形です。JavaScript が読み込まれる前に押されても送信できます。

import { save } from './actions'
 
export default function Page() {
  return (
    <form action={save}>
      <input name="name" />
      <button type="submit">保存</button>
    </form>
  )
}

この形は Server Component のままで書けます。'use client' は要りません。

#結果を受け取る — useActionState

保存が成功したか、どこが間違っていたかを画面に出したいときは useActionState を使います。フックなので、これを書いたファイルは Client Component になります。

'use client'
 
import { useActionState } from 'react'
import { save } from './actions'
 
export function Form() {
  const [message, action] = useActionState(save, '')
 
  return (
    <form action={action}>
      <input name="name" />
      <button type="submit">保存</button>
      <p>{message}</p>
    </form>
  )
}

引数は「前の状態」が先

ここが一番間違えやすいところです。useActionState に渡した関数は、第1引数に前の状態、第2引数にフォームの内容を受け取ります。

同じ関数を useActionState に渡したときの、引数の受け取り方
うまくいかない
'use server'
 
// フォームの内容が来るつもりで書いている
export async function save(formData: FormData) {
const name = formData.get('name')
return '保存しました'
}

formData には前の状態(文字列)が入るので、get を呼んだ時点で落ちる。

こう書く
'use server'
 
// 第1引数は前の状態。フォームの内容は第2引数
export async function save(prev: string, formData: FormData) {
const name = formData.get('name')
return '保存しました'
}

prev には前回の戻り値が入る。初回は useActionState の第2引数の値。

実機で確かめました。useActionState に渡した関数を、引数の中身を報告するだけの関数に差し替えてブラウザから送信すると、第1引数は文字列、第2引数は FormData でした。

型で止まる。止まらない構成なら、送信するまで気づけない

引数の順番を間違えた関数を useActionState に渡すと、TypeScript が止めてくれます。実際に当てて確かめました。

Argument of type '(formData: FormData) => Promise<string>' is not assignable
to parameter of type '(state: string) => string | Promise<string>'.

止まらないのは、next build の型検査を切っている構成のときと、引数を any で受けているときです。その場合はビルドが通り、落ちるのはブラウザで送信ボタンが押された瞬間になります。

型検査をすり抜けたときに起きること
  1. Build
    ビルドは成功する

    型検査を切っていれば、引数の食い違いは残ったまま通る。

  2. Browser
    送信される

    ここまでは何も起きない。

  3. Server
    関数の中で例外が出る

    文字列に対して get を呼んでいるので落ちる。応答は 500。

  4. Browser
    理由は出ない

    本番ビルドでは、例外の内容はブラウザに送られない。後で詳しく扱う。

#送信中を出す — useFormStatus

useFormStatus は、送信中かどうかを教えてくれるフックです。ただし置き場所に強い制約があります。

同じ useFormStatus でも、どこで呼ぶかで結果が変わる
うまくいかない
'use client'
 
export function Panel() {
// form を書いているのはこのコンポーネント自身
const { pending } = useFormStatus()
 
return (
  <form action={save}>
    <button disabled={pending}>保存</button>
  </form>
)
}

pending はずっと false。ボタンは押せたままで、二重送信も止まらない。

こう書く
'use client'
 
function Submit() {
// form の中に描かれる部品として呼ぶ
const { pending } = useFormStatus()
return <button disabled={pending}>保存</button>
}
 
export function Panel() {
return (
  <form action={save}>
    <Submit />
  </form>
)
}

送信中だけ pending が true になる。

実機で確かめました。同じページの中で、フォームを書いた側と、フォームの中に置いた部品の両方で useFormStatus を呼び、送信中に値がどうなったかを記録しました。結果は外側は最後まで false、内側だけが true でした。

エラーが出るわけではないので、気づきにくい間違いです。ボタンが押せたままになるので、二重送信の防止だと思って書いたものが効いていないという形で表に出ます。

#先に表示する — useOptimistic

useOptimistic は、サーバーの返事を待たずに「たぶんこうなる」という表示を先に出す仕組みです。

'use client'
 
import { useOptimistic } from 'react'
 
export function List({ items }: { items: string[] }) {
  const [shown, addOptimistic] = useOptimistic(
    items,
    (state: string[], next: string) => [...state, next],
  )
 
  async function handle(formData: FormData) {
    addOptimistic(String(formData.get('name')))
    await save(formData)
  }
 
  return <form action={handle}>{/* … */}</form>
}

実機で確かめました。['a'] を渡した状態で追加すると、表示は a → a と temp → a と動きました。真ん中が楽観表示で、最後に元へ戻っています。

つまり useOptimistic は「速く見せる」仕組みであって、「保存する」仕組みではありません。アクションの中で本当にデータを更新し、画面を作り直させるところまでが1組です。

楽観表示が消えるまで
  1. Browser
    先に表示する

    サーバーへ送る前に、追加された状態を描く。

  2. Server
    実際の更新が走る

    ここで本当に保存し、画面を作り直させる。

  3. Browser
    楽観表示は捨てられる

    残るのは渡した値だけ。更新していなければ元の表示に戻る。

#フォームを使わずに呼ぶ

Server Function は、フォームがなくても呼べます。ボタンのイベントハンドラーの中で、普通の非同期関数として呼ぶだけです。

'use client'
 
import { remove } from './actions'
 
export function DeleteButton({ id }: { id: number }) {
  return <button onClick={() => remove(id)}>削除</button>
}

実機で確かめました。この形で呼んだ戻り値は、そのままブラウザに返ってきます。

ただし、この形では送信中も失敗も自分で面倒を見ることになります。useFormStatus は効きませんし、例外を受け止めるのも自分の仕事です。押せる回数を止めないと、連打でそのまま連打の数だけ呼ばれます。

渡せない値を渡しても、エラーにはならない

引数は境界を越えます。DOM の要素やイベントオブジェクトのように、越えられない値を渡したときの挙動は、少し変わっています。

// ❌ 入力欄そのものを渡してしまっている
<button onClick={() => rename(ref.current)}>変更</button>

実機で確かめました。この形はビルドも通り、呼び出しもサーバーに届きます。渡した DOM 要素は、サーバー側では「一時的な参照」という触れないものになって現れます。

境界を越えられない値を引数にしたとき
  1. Build
    ビルドは通る

    引数の中身が越えられるかどうかは、ここでは分からない。

  2. Browser
    呼び出しは届く

    値は「一時的な参照」に置き換えられて運ばれる。

  3. Server
    触った瞬間に落ちる

    参照の中身を読もうとすると例外になる。応答は 500。

サーバー側のログにはこう出ます。

Cannot access value on the server. You cannot dot into a temporary client
reference from a server component. You can only pass the value through to
the client.

これはブラウザに何が届くのかで扱っている方針と同じものです。サーバーの中の事情は、頼まれない限り境界を越えません。

引数に渡してよいのは、値そのものです。要素ではなく、その中の文字列を渡してください。

// ✅ 値だけを渡す
<button onClick={() => rename(ref.current?.value ?? '')}>変更</button>

渡せる値の制限は、props のときと同じです。詳しくはServer から Client へ渡せる値にあります。

#まとめ

  • フォームの action に渡すのが基本。この形だけは Server Component のまま書ける
  • useActionState に渡した関数は、第1引数が前の状態、第2引数がフォームの内容
  • 引数の順番を間違えると TypeScript が止める。型検査を切っていると送信するまで気づけない
  • useFormStatusフォームの中に描かれる部品でしか動かない
  • useOptimistic の値は必ず捨てられる。サーバー側を更新しなければ表示は戻る
  • イベントハンドラーからも呼べる。ただし送信中と失敗の扱いは自分で書く
  • 越えられない値を引数にしてもその場では落ちない。サーバーで触った瞬間に落ちる

関数を渡すところまでの話はServer Function の渡し方に、境界を越えられない関数の話はEvent handlers cannot be passedにあります。

出典

このページの内容は上記の一次情報を基準に書いています。 個人ブログや二次情報を根拠にしていません。

理解できたか試す

では、このコードはどうなりますか。

useActionState に渡した Server Function が実際に何を受け取るかを調べています。ブラウザから送信すると、戻り値はどうなりますか。

この問題を解くこのテーマの問題 5