0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.js(App Router)での検索機能について

Last updated at Posted at 2025-04-23

概要

自身が開発しているブログアプリに検索機能を追加しました :tada:

検索コンポーネントの検索結果に応じて、ページ内のpostsデータをフィルタリングするUXを実現したかったのですが、実装方針に迷いがあり、記事にまとめました。

image.png

結論

以下の流れで機能実装しました。

  1. SearchBoxClient Component)で文字列変更時にrouter.push
  2. page.tsxで検索文字列に応じたpostsを再取得

実装

それでは実装していきます。
今回の実装では、私のアプリで使用している一部コンポーネントの記述を抜き出して解説しています。
適宜自身の環境に置き換えてご確認ください🙇‍♂️

SearchBox.tsx

検索を行う SearchBox を実装します。

入力を監視し、デバウンス処理を通じてクエリ付きのURLに遷移することで、検索結果を反映させます。

'use client';

import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import styles from './SearchBox.module.css';

export default function SearchBox() {
  const searchParams = useSearchParams();
  const [search, setSearch] = useState(searchParams.get('q') || '');
  const [debouncedSearch, setDebouncedSearch] = useState(search);
  const router = useRouter();

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedSearch(search);
    }, 500);

    return () => clearTimeout(timer);
  }, [search]);

  useEffect(() => {
    if (debouncedSearch.trim()) {
      router.push(`/?q=${debouncedSearch.trim()}`);
    } else if (debouncedSearch === '') {
      router.push('/');
    }
  }, [debouncedSearch, router]);

  return (
    <div className={styles.searchContainer}>
      <div className={styles.searchBox}>
        <input
          type="text"
          placeholder="記事を検索..."
          className={styles.searchInput}
          value={search}
          onChange={(e) => setSearch(e.target.value)}
        />
        <div className={styles.searchIcon}>
          <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
            <path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>
          </svg>
        </div>
      </div>
    </div>
  );
}

page.tsx

page.tsx では、URLのクエリパラメータ(searchParams)を元に、検索キーワードが存在するかどうかを判定しています。

  • 検索キーワードがある場合は searchPosts() を実行
  • ない場合は通常の getPosts() を実行

といった条件分岐で表示する投稿データを切り替えています。

import Card from "@/components/ui/Card/Card";
import styles from './page.module.css';
import Link from "next/link";
import { getPosts, searchPosts } from "@/app/(public)/posts/fetcher";
import { PostsResponse } from "../types";

type PageProps = {
  searchParams: Promise<{
    cursor?: string;
    q?: string;
  }>;
};

export default async function Home({ searchParams }: PageProps) {
  const queryParams = await searchParams;
  const cursor = queryParams.cursor || undefined;
  const searchQuery = queryParams.q || '';

  const data = searchQuery
    ? await searchPosts({ query: searchQuery, first: 15, after: cursor })
    : await getPosts({ first: 15, after: cursor });

  const posts = await data.json() as PostsResponse;

  return (
    <div>
      {searchQuery && (
        <h1 className={styles.searchTitle}>{searchQuery}」の検索結果</h1>
      )}

      {posts.edges.length === 0 && searchQuery && (
        <p className={styles.noResults}>検索結果が見つかりませんでした。別のキーワードをお試しください。</p>
      )}

      <div className={styles.cardContainer}>
        {posts.edges.map(({ node: post }) => (
          <Link href={`/posts/${post.id}`} key={post.id}>
            <Card
              img={post.thumbnailUrl}
              title={post.title}
              description={post.content}
              variant="post"
              unoptimized={true}
              maxLines={3}
              titleMaxLines={true}
            />
          </Link>
        ))}
      </div>

      <div className={styles.paginationContainer}>
        {posts.pageInfo.hasPreviousPage && (
          <Link href={`/?${searchQuery ? `q=${searchQuery}&` : ''}cursor=${posts.pageInfo.startCursor}`} className={styles.paginationLink}>
            前のページ
          </Link>
        )}
        {posts.pageInfo.hasNextPage && (
          <Link href={`/?${searchQuery ? `q=${searchQuery}&` : ''}cursor=${posts.pageInfo.endCursor}`} className={styles.paginationLink}>
            次のページ
          </Link>
        )}
      </div>
    </div>
  );
}

まとめ

今までNextではプルダウン形式の検索機能しか採用していなかった...
日々学びですね 🔍

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?