Server Function の呼び方
フォームの action に渡すのが基本の形です。
#呼び方は3つある
Server Function の渡し方は、境界に穴を開けるところまでの話でした。ここでは、開けた穴をクライアントからどう呼ぶかを扱います。
| 書き方 | 送信中が分かる | 結果を受け取れる |
|---|---|---|
| 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引数にフォームの内容を受け取ります。
'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 で受けているときです。その場合はビルドが通り、落ちるのはブラウザで送信ボタンが押された瞬間になります。
- Buildビルドは成功する
型検査を切っていれば、引数の食い違いは残ったまま通る。
- Browser送信される
ここまでは何も起きない。
- Server関数の中で例外が出る
文字列に対して get を呼んでいるので落ちる。応答は 500。
- Browser理由は出ない
本番ビルドでは、例外の内容はブラウザに送られない。後で詳しく扱う。
#送信中を出す — 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組です。
- Browser先に表示する
サーバーへ送る前に、追加された状態を描く。
- Server実際の更新が走る
ここで本当に保存し、画面を作り直させる。
- 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 要素は、サーバー側では「一時的な参照」という触れないものになって現れます。
- Buildビルドは通る
引数の中身が越えられるかどうかは、ここでは分からない。
- Browser呼び出しは届く
値は「一時的な参照」に置き換えられて運ばれる。
- 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にあります。
出典
- react.dev/reference/react/useActionState
- react.dev/reference/react-dom/hooks/useFormStatus
- react.dev/reference/react/useOptimistic
- nextjs.org/docs/app/api-reference/directives/use-server
- nextjs.org/docs/app/guides/forms
このページの内容は上記の一次情報を基準に書いています。 個人ブログや二次情報を根拠にしていません。