Next.js のフォーム送信。入出力の最小構成サンプル
入力を受け取って結果を返すだけなら、ファイルは4つで足ります。
動いているものを触る/demo/ioファイル構成
並んでいるのは、上の画面をいま動かしている本物のファイルです。名前を押すと中身が出ます。
- app/
- done/
全ページの外側。本物のプロジェクトでは html と body をここで書きます
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "入出力の最小構成",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return <main>{children}</main>;
}- .tsx
- 画面を返すファイル。HTML のような書き方ができます
- .ts
- 画面を返さないファイル。処理とデータだけを置きます。ここに画面を書くと原因を名指ししないエラーで落ちます
- Build
- ビルド時に一度だけ動きます
- Server
- アクセスのたびにサーバーで動きます
- Browser
- 利用者の手元で動きます
4つで足りる
「入力を受け取って、結果を返す」だけなら、必要なのはページが2つと、受け取る関数が1つと、外側が1つです。
- Browserフォームを送る
普通の form の送信。JavaScript が読み込まれる前でも押せる。
- Serveractions.ts が受け取る
値を切り詰めて、結果のページへ送り出す。この関数の中身はブラウザに届かない。
- Serverdone/page.tsx が描く
受け取った値を出す。エスケープは React がやる。
'use client' はどこにも書いていません。状態もイベントも使っていないからです。
入力はどこで受け止めるか
app/actions.ts の先頭に 'use server' があります。これは「この関数をクライアントから呼べるようにする」という指示で、境界に穴を1つ開ける操作です。
穴である以上、画面を経由せずに誰でも呼べます。だから受け取った値は、その関数の中で必ず絞ります。
const text = String(formData.get('text') ?? '')
.trim()
.slice(0, 40)
入力欄に maxLength を書いてありますが、それは目安であって守りではありません。ブラウザ側の指定は簡単に外せます。長さを決めているのはサーバー側のこの行です。
なぜ結果を別のページにしたか
同じページで結果まで出そうとすると、状態が要ります。状態が要るとフックが要り、フックが要ると 'use client' が要ります。
送り先を別のページにすれば、その連鎖が起きません。送信のあとに redirect で移るこの形は、ブラウザの戻る操作とも噛み合います。
同じページに出したい場合は、Server Function の呼び方にある useActionState を使います。そのときは、そのファイルだけが Client Component になります。
ここから足していくとき
- 保存するなら、
actions.tsの中でデータベースへ書きます。ページ側は変わりません - 入力の形を厳しく見るなら、同じ場所で検証ライブラリを通します
- 結果を同じ画面に出したくなったら、その部分だけを Client Component へ切り出します
足す場所はいつも同じで、actions.ts の中か、切り出した葉の部分です。ページを丸ごと Client Component にする必要は最後まで出てきません。
入れていないもの
このまま本番に持っていかないでください。省いてあるのは次の点です。
- データベース。受け取った値はどこにも保存していません
- 入力の細かい検証。長さを切るだけで、形式は見ていません
- 失敗したときの画面。ここでは元のページへ戻すだけです
- 設定ファイル。package.json と next.config.ts は作られたままです
出典
- nextjs.org/docs/app/guides/forms
- nextjs.org/docs/app/api-reference/directives/use-server
- nextjs.org/docs/app/api-reference/functions/redirect
このページの内容は上記の一次情報を基準に書いています。個人ブログや二次情報を根拠にしていません。