【Next.js】v15から導入されたFormコンポーネント(next/form)使って検索フォームを作成してみた

【Next.js】v15から導入されたFormコンポーネント(next/form)使って検索フォームを作成してみた

Clock Icon2024.11.26

リテールアプリ共創部のるおんです。Next.js v15からNext.jsが提供するFormコンポーネントが利用できるようになりました。これは従来のformタグを拡張した独自コンポーネントであり、従来の定型的なコードを減らすことができるので、URL検索パラメータを更新するフォームに便利です。

今回はこのFormコンポーネントを使用して簡易的な検索フォームを実装してみたので共有したいと思います。

<Form>コンポーネントとは

https://nextjs.org/docs/app/api-reference/components/form

Next.jsのバージョン15から導入された <Form>コンポーネントは、公式ドキュメントによると以下の特徴があります。

  • クエリパラメーター変更によるクライアントサイドナビゲーション(ソフトナビゲーション)の自動化
  • URLパラメータの自動管理
  • 共通UIのプリフェッチ処理(layout.tsxなど)

基本的な使い方

公式ドキュメントからそのまま抜粋です。

search.tsx
import Form from 'next/form'

export default function Page() {
  return (
    <Form action="/search">
      {/* On submission, the input value will be appended to 
          the URL, e.g. /search?query=abc */}
      <input name="query" />
      <button type="submit">Submit</button>
    </Form>
  )
}

ボタンで送信すると、action内の文字列/searchにURLのpathが遷移します。さらに、inputタグのname属性に入れた値のクエリパラメーターを作成してくれます。
つまり、上のフォームにああああといれて送信すると、https://example.com/search?query=ああああにクライアントサイドナビゲーション(ソフトナビゲーション)してくれます。

さらに、HTMLのformタグを拡張しているのでプログレッシブエンハンスメントにも対応しています。

また、actionに関数を渡すことでServer Actionとしても機能させることができるので今後は従来のformの代わりにこちらのFormコンポーネントを使用することになりそうです。今回は検索フォームとして実装したいのでこちらの機能は使用しません

個人的にこのFormコンポーネントの一嬉しいのは、特徴として最初にあげた クライアントサイドナビゲーションの自動化 です。

従来のformタグだと、ボタンを送信した瞬間に特定のpathにGetリクエストを送信してしまうのでページ全体がリロードされて自動的にハードナビゲーションになってしまいます。そうすると、画面全体が更新されてしまうので、保持していた状態やページがリセットされてしまうので嬉しくありません。

また、それを防ぐためにクライアントサイドナビゲーションにしようとしたらuseRouterを使用する必要があり、自前でロジックを組む必要があります。またフックを使用するのでClient Componentを余儀されてしまいます。
以下は、useRouterを使用した場合の検索フォームのロジック部分の実装例です。

従来の例
search.tsx
"use client";
import { useRouter } from "next/navigation";
import { useState } from "react";

export default function Page() {
+ const router = useRouter();
  // 検索フォームの状態
  const [query, setQuery] = useState<string>("");

  const handleSubmit = () => {
    const params = {
      query,
    };
    const queryString = new URLSearchParams(params).toString();
    // クエリパラメータをURLに追加
+   router.push(`/search?${queryString}`);
  };
  return (
    <div>
      <input name="query" onChange={(e) => setQuery(e.target.value)} />
      <button type="submit" onClick={handleSubmit}>
        Submit
      </button>
    </div>
  );
}

この例は非常にシンプルなものですが、実務での検索フォームはより複雑でかなり難解なコードを実装する必要があります。

実際に検索フォームを作ってみた

プロジェクトの概要

デモ動画
今回は、洋服ECサイトにありそうな簡易的な検索フォームを作成します。ユーザーはカテゴリー、価格、ブランドで商品を検索できます。完成した画面の挙動は以下の動画をご覧ください。

https://youtu.be/eB-P9XOs914

実装

本来ならDBなどに洋服のデータが格納されていると思いますが、今回は洋服データのモックを作成しておきます。

モックデータ
data.ts
export type Clothes = {
  id: string;
  name: string;
  price: number;
  category: string;
  size: ("S" | "M" | "L" | "XL")[];
  color: string;
  brand: string;
  stock: number;
  description: string;
  imageUrl: string;
};

export const mockData: Clothes[] = [
  {
    id: "1",
    name: "クラシック白シャツ",
    price: 5900,
    category: "トップス",
    size: ["S", "M", "L", "XL"],
    color: "白",
    brand: "Basic Style",
    stock: 50,
    description: "上質なコットン素材を使用したベーシックな白シャツ",
    imageUrl: "/images/white-shirt.jpg"
  },
  {
    id: "2",
    name: "スリムフィットジーンズ",
    price: 8900,
    category: "ボトムス",
    size: ["S", "M", "L", "XL"],
    color: "インディゴ",
    brand: "Denim Co.",
    stock: 35,
    description: "ストレッチ素材で快適な穿き心地のスリムジーンズ",
    imageUrl: "/images/slim-jeans.jpg"
  },
  {
    id: "3",
    name: "カシミヤセーター",
    price: 15900,
    category: "トップス",
    size: ["M", "L"],
    color: "グレー",
    brand: "Luxury Knit",
    stock: 20,
    description: "上質なカシミヤ100%の暖かいニットセーター",
    imageUrl: "/images/cashmere-sweater.jpg"
  },
  {
    id: "4",
    name: "テーラードジャケット",
    price: 23900,
    category: "アウター",
    size: ["M", "L", "XL"],
    color: "ネイビー",
    brand: "Classic Tailor",
    stock: 15,
    description: "ビジネスからカジュアルまで対応できる万能ジャケット",
    imageUrl: "/images/blazer.jpg"
  },
  {
    id: "5",
    name: "プリーツスカート",
    price: 7900,
    category: "ボトムス",
    size: ["S", "M", "L"],
    color: "ブラック",
    brand: "Elegant Style",
    stock: 25,
    description: "上品な光沢のある素材を使用したプリーツスカート",
    imageUrl: "/images/pleated-skirt.jpg"
  },
  {
    id: "6",
    name: "フローラルワンピース",
    price: 12900,
    category: "ワンピース",
    size: ["S", "M", "L", "XL"],
    color: "花柄",
    brand: "Floral Collection",
    stock: 30,
    description: "春夏シーズンにぴったりの花柄ワンピース",
    imageUrl: "/images/floral-dress.jpg"
  },
  {
    id: "7",
    name: "Vネックカーディガン",
    price: 6900,
    category: "トップス",
    size: ["S", "M", "L"],
    color: "ベージュ",
    brand: "Comfort Wear",
    stock: 40,
    description: "軽くて着心地の良いベーシックカーディガン",
    imageUrl: "/images/cardigan.jpg"
  }
];

次に、こちらが実装した全体のコードです。

search/page.tsx
import Form from "next/form";
import { mockData } from "../_data/clothes";

export default function Page({
  searchParams,
}: {
  searchParams: {
    [key: string]: string | string[] | undefined;
  };
}) {
  const query = {
    category: searchParams.category?.toString().toLowerCase() || "",
    price: searchParams.price?.toString() || "",
    brand: searchParams.brand?.toString().toLowerCase() || "",
  };

  const filteredData = mockData.filter((item) => {
    return (
      (!query.category ||
        item.category.toLowerCase().includes(query.category)) &&
      (!query.price || item.price <= parseInt(query.price)) &&
      (!query.brand || item.brand.toLowerCase().includes(query.brand))
    );
  });

  return (
    <div className="flex min-h-screen">
+     {/* 検索フォーム */}
+     <div className="w-1/4 p-4 border-r">
+       <Form action="" className="space-y-4">
+         <input
+           name="category"
+           defaultValue={query.category}
+           placeholder="カテゴリーで検索..."
+           className="w-full p-2 border rounded"
+         />
+         <input
+           name="price"
+           defaultValue={query.price}
+           placeholder="価格で検索..."
+           className="w-full p-2 border rounded"
+           type="number"
+        />
+         <input
+           name="brand"
+           defaultValue={query.brand}
+           placeholder="ブランドで検索..."
+           className="w-full p-2 border rounded"
+         />
+         <button
+           type="submit"
+           className="w-full bg-blue-500 text-white p-2 rounded hover:bg-blue-600"
+         >
+           検索
+         </button>
+       </Form>
+     </div>

      {/* 検索結果 */}
      <div className="w-3/4 p-4">
        <h2 className="text-xl font-bold mb-4">
          検索結果: {filteredData.length}        </h2>
        <div className="space-y-4">
          {filteredData.map((item) => (
            <div key={item.id} className="border p-4 rounded shadow">
              <h3 className="font-bold">{item.name}</h3>
              <p>価格: ¥{item.price.toLocaleString()}</p>
              <p>カテゴリー: {item.category}</p>
              <p>ブランド: {item.brand}</p>
              <p>在庫数: {item.stock}</p>
              <p className="text-sm text-gray-600">{item.description}</p>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

セグメントのpage.tsxでは引数としてsearchParamsを受け取ることができます。これによりクエリパラメーターを取得することができ、それを使用して洋服データから検索結果をフィルタリングしています。
また、Formコンポーネントにaction=""と空文字を入れることで同一ページ内でクエリパラメーターのみをセットすることができます。

動作確認

フォームに入力して検索を押すと実際にクエリパラメーターがセットされるのが確認できると思います。

スクリーンショット 2024-11-26 19.03.08
Formコンポーネントを使用するだけでかなり楽にクエリパラーメータをセットすることができ、画面の状態を保持したまま再度サーバーコンポーネントからデータを取得することができました!

おわりに

next/formのFormコンポーネントの導入により、フォーム実装の複雑さが劇的に軽減されました。

これまでのフォーム実装では毎回同じような冗長なコードを書く必要がありましたが、Next.jsの進化によって開発体験が大きく改善されています。
今後のプロジェクトでは、この新機能を積極的に活用していきたいと思います。

参考

https://nextjs.org/docs/app/api-reference/components/form

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.