📄

【Next.js x Conform x nuqs】1コンポーネントで実現する入力フォーム・確認画面パターン

に公開

はじめに

Webアプリケーション開発で入力項目の多いお問い合わせや申請系のフォームを実装するとき、ユーザーが入力内容を振り返れるよう確認画面を用意することも多いかと思います。
本記事では、この入力フォーム・確認画面パターンをNext.js(App Router)、Conformnuqsで極力簡単に実現する方法を紹介します。

入力フォーム・確認画面パターンの例

今回の実装の特徴

  • URL(ページ)は1つで実現(例: /contacts/new)
  • 入力フォーム・確認画面を1コンポーネントでシンプル実装
  • クエリパラメータで入力フォーム・確認画面の表示状態を管理(例: /contacts/new?view=confirm)
  • ブラウザバック(確認画面 → 入力フォーム)も正常に機能
  • サーバーアクションのエラー発生時に入力フォームに切り替えてエラー表示
  • Conformの特徴を崩さずフル活用(フォーム値をDOMで管理、サーバーアクション連携、クライアント&サーバーサイドで同一バリデーションロジック利用、などなど)

実装詳細

// Conformのバリデーションで利用するzodスキーマ
export const zodSchema = z.object({...});
// Conformと統合されたサーバーアクション
export const serverAction = (prevState, formData): Promise<SubmissionResult> => {...};

// form.tsx
export const ContactForm = () => {
  // 画面の表示タイプ(view)をクエリパラメータで管理
  const [view, setView] = useQueryState(
    "view",
    parseAsStringLiteral(["form", "confirm"]) // form: 入力フォーム, confirm: 確認画面
      .withDefault("form") // デフォルトは「入力フォーム」
      .withOptions({ history: "push", scroll: true }), // 履歴は追加。画面の切替時に一番上までスクロール
  );

  // フォームアクション送信の状態管理
  const [lastResult, formAction, isPending] = useActionState(
    // 送信結果がエラーの場合に表示タイプを「入力フォーム」に変更するため、サーバーアクションをラップした関数を渡す
    async (prevState, formData) => {
      const result = await serverAction(prevState, formData);
      // 送信結果がエラーの場合は表示タイプを「入力フォーム」に変更
      if (result.status === "error") setView("form");
      return result;
    },
    undefined,
  );

  // Conformでフォームを管理
  const [form, fields] = useForm({
    lastResult, // 前回の送信結果を同期
    onValidate: ({ formData }) => parseWithZod(formData, { schema: zodSchema }), // クライアントバリデーション
    onSubmit(event, { formData }) {
      // 「確認画面へ」ボタンが押された時、フォーム送信を中断し表示タイプを「確認画面」に変更
      if (formData.get("intent") === "display-confirm") {
        event.preventDefault();
        setView("confirm");
      }
    },
  });

  return (
    <form {...getFormProps(form)} action={formAction}>
      {/* 入力フォーム */}
      <div
        style={{
          // Conformは入力値をDOMで管理するため、表示の切り替えはスタイルを当てて対応する
          display: view === "confirm" ? "none" : "block",
        }}
      >
        <input {...getInputProps(fields.field1)} />
        <input {...getInputProps(fields.field2)} />
        <input {...getInputProps(fields.field3)} />
        <button type="submit" name="intent" value="display-confirm">
          確認画面へ
        </button>
      </div>

      {/* 確認画面 */}
      {view === "confirm" && (
        <div>
          <p>フィールド1:{fields.field1.value}</p>
          <p>フィールド2:{fields.field2.value}</p>
          <p>フィールド3:{fields.field3.value}</p>
          <div>
            <button type="button" onClick={() => setView("form")}>
              修正する
            </button>
            <button type="submit">
              送信する
            </button>
          </div>
        </div>
      )}
    </form>
  );
};

実装のポイント

  • 画面の表示タイプをnuqsで状態管理
    • viewパラメータで入力フォームと確認画面の表示切り替え
    • history: "push"で表示切り替え時の履歴を残しブラウザバックに対応
  • サーバーアクションをクライアント側でラップして、送信結果がエラーの場合に入力フォームに表示を切り替える
  • インテントボタンの利用
    • intent: "display-confirm"を「確認画面へ」ボタンに設定してサブミットを区別
    • 「確認画面へ」ボタンによるサブミットは処理を中断し確認画面に表示を切り替える
  • サブミットボタンが機能するよう入力フォームと確認画面のマークアップはformタグの中に用意する
  • Conformは入力情報をDOMで管理するため、入力フォームのDOM要素は残しておく

デモ

まとめ

いかがだったでしょうか?
Next.js, Conform, nuqsをうまく組みわせてシンプルに入力フォーム・確認画面パターンを実現することができました。
コーディングのヒントになれば幸いです。

Discussion