Search Paramsによる検索機能

2023年5月28日
nextjs
react
JavaScript
TypeScript
検索パラメータを使用してサーバーサイドで検索します。

本記事の完成品です。

Your Title

Your Content

View

はじめに

この記事では、Next.js の Server Components を使って、検索機能を実装します。 サーバーサイドで検索を行うことで、セキュリティ面の向上に繋がり、クライアントの JavaScript の負荷を軽減することができます。

以下このブログサイトの検索機能を例に、実装方法を解説します。

page.tsx

export default async function BlogPage({ searchParams }: { searchParams: { search?: string } }) {
  const searchQuery = searchParams.search ?? '';
 
  let blogs: Blog[] = [];
 
  // ブログ記事のデータを取得
  const initialBlogsData = allBlogs
    .filter((post) => post.published)
    .sort((a, b) => {
      return compareDesc(new Date(a.date), new Date(b.date));
    });
 
  // 検索クエリに一致する記事を抽出
  const filteredBlogsData = initialBlogsData.filter((blog) => {
    const title = blog.title.toLowerCase();
    const tags = blog.tags.map((tag) => tag.toLowerCase());
    const search = searchQuery.toLowerCase();
    return title.includes(search) || tags.includes(search);
  });
 
  if (searchQuery.length > 0) {
    if (filteredBlogsData) {
      blogs = filteredBlogsData;
    } else {
      blogs = [];
    }
  } else {
    blogs = initialBlogsData ?? [];
  }
 
  return (
    <>
      <h1 className="text-4xl font-bold">記事一覧</h1>
      <p className="text-lg text-neutral-500 pt-2">執筆中の記事を一覧で表示します。</p>
      <SearchServerParams />
      <hr className="my-8" />
      {blogs.length ? (
        <div className="grid gap-10 grid-cols-1 sm:grid-cols-2">
          <BlogsList blogs={blogs} />
        </div>
      ) : (
        <p>記事が見つかりませんでした。</p>
      )}
    </>
  );
}

このコードはpage.tsxで Server Components の引数にsearchParamsを追加したものです。 検索パラメータを URL に追加することで、サーバーサイドで検索を行うことを可能にします。

Input コンポーネントで文字が入力される度に、searchParamsの値を更新します。 そのため、searchParamsの値が変更される度に、変数の blogs も更新され、検索結果が表示されます。

SearchServerParams.tsx

'use client';
 
import { useCallback, useEffect, useState, useTransition } from 'react';
import { usePathname, useRouter } from 'next/navigation';
 
import { Input } from '../ui/input';
import Spinner from '../ui/spinner';
 
const SearchServerParams = () => {
  const [inputValue, setInputValue] = useState<string>('');
  const [debouncedValue, setDebouncedValue] = useState<string>('');
  const [mounted, setMounted] = useState<boolean>(false);
  const router = useRouter();
  const pathname = usePathname();
  const [isPending, startTransition] = useTransition();
 
  const handleSearchParams = useCallback(
    (debouncedValue: string) => {
      let params = new URLSearchParams(window.location.search);
      if (debouncedValue.length > 0) {
        params.set('search', debouncedValue);
      } else {
        params.delete('search');
      }
      startTransition(() => {
        router.replace(`${pathname}?${params.toString()}`);
      });
    },
    [pathname, router],
  );
 
  useEffect(() => {
    const params = new URLSearchParams(window.location.search);
    const searchQuery = params.get('search') ?? '';
    setInputValue(searchQuery);
  }, []);
 
  useEffect(() => {
    if (debouncedValue.length > 0 && !mounted) {
      setMounted(true);
    }
  }, [debouncedValue, mounted]);
 
  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(inputValue);
    }, 500);
 
    return () => {
      clearTimeout(timer);
    };
  }, [inputValue]);
 
  useEffect(() => {
    if (mounted) handleSearchParams(debouncedValue);
  }, [debouncedValue, handleSearchParams, mounted]);
 
  return (
    <div className="relative mt-8 mb-5">
      <Input
        value={inputValue}
        onChange={(e) => {
          setInputValue(e.target.value);
        }}
        placeholder="Search Blog"
        className="text-base"
      />
      {isPending && (
        <div className="absolute top-2 right-2">
          <Spinner />
        </div>
      )}
    </div>
  );
};
 
export default SearchServerParams;

ここでのコードはやや複雑ですが、順を追って見ていきましょう。

const handleSearchParams = useCallback(
  (debouncedValue: string) => {
    let params = new URLSearchParams(window.location.search);
    if (debouncedValue.length > 0) {
      params.set('search', debouncedValue);
    } else {
      params.delete('search');
    }
    startTransition(() => {
      router.replace(`${pathname}?${params.toString()}`);
    });
  },
  [pathname, router],
);

まず、handleSearchParamsでは、debouncedValueの値をsearchクエリパラメータにセットします。 これは、debouncedValueが変更される度に呼び出されます。

useEffect(() => {
  const params = new URLSearchParams(window.location.search);
  const searchQuery = params.get('search') ?? '';
  setInputValue(searchQuery);
}, []);

次に、useEffectを用いて、searchクエリパラメータの値をinputValueにセットします。 これは、ページが読み込まれた時に呼び出されます。

useEffect(() => {
  if (debouncedValue.length > 0 && !mounted) {
    setMounted(true);
  }
}, [debouncedValue, mounted]);

次に、useEffectを用いて、debouncedValueの値が変更された時に、mountedの値をtrueにします。

useEffect(() => {
  const timer = setTimeout(() => {
    setDebouncedValue(inputValue);
  }, 500);
 
  return () => {
    clearTimeout(timer);
  };
}, [inputValue]);

次に、useEffectを用いて、inputValueの値が変更された時に、debouncedValueの値をinputValueにセットします。 これは、inputValueが変更される度に呼び出されます。

useEffect(() => {
  if (mounted) handleSearchParams(debouncedValue);
}, [debouncedValue, handleSearchParams, mounted]);

最後に、useEffectを用いて、debouncedValueの値が変更された時に、handleSearchParamsを呼び出します。 これは、debouncedValueが変更される度に呼び出されます。

これらのuseEffectの呼び出し順は、以下のようになります。

  1. ページが読み込まれた時に、searchクエリパラメータの値をinputValueにセットする。
  2. inputValueの値が変更された時に、debouncedValueの値をinputValueにセットする。
  3. debouncedValueの値が変更された時に、handleSearchParamsを呼び出す。
  4. debouncedValueの値が変更された時に、mountedの値をtrueにする。
  5. debouncedValueの値が変更された時に、handleSearchParamsを呼び出す。

まとめ

今回は、useEffectを用いて、searchクエリパラメータの値をinputValueにセットする方法を学びました。

参考