概要
useDebounce カスタムフックを useEffect + setTimeout で自作し、検索入力のデバウンス処理を実装する。lodash.debounce を使わず外部依存なしで実現することで、仕組みを理解しやすくする。入力が止まった後に API リクエストを送るパターンを示す。
インストール
# 追加インストールは不要
実装
useDebounce フック
// hooks/useDebounce.ts
import { useEffect, useState } from "react";
export function useDebounce<T>(value: T, delay: number = 300): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// 次の effect 実行前(または アンマウント時)にタイマーをキャンセル
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
検索コンポーネント
// components/SearchBox.tsx
"use client";
import { useEffect, useState } from "react";
import { useDebounce } from "@/hooks/useDebounce";
type Result = { id: number; title: string };
export function SearchBox() {
const [query, setQuery] = useState("");
const [results, setResults] = useState<Result[]>([]);
const [isSearching, setIsSearching] = useState(false);
const debouncedQuery = useDebounce(query, 400);
useEffect(() => {
if (!debouncedQuery.trim()) {
setResults([]);
return;
}
setIsSearching(true);
fetch(
`https://jsonplaceholder.typicode.com/posts?title_like=${encodeURIComponent(debouncedQuery)}&_limit=5`
)
.then((res) => res.json())
.then((data: Result[]) => {
setResults(data);
})
.finally(() => {
setIsSearching(false);
});
}, [debouncedQuery]);
return (
<div className="w-full max-w-md">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="キーワードを入力..."
className="w-full rounded border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{isSearching && (
<p className="mt-2 text-sm text-gray-400">検索中...</p>
)}
{!isSearching && results.length > 0 && (
<ul className="mt-2 space-y-1">
{results.map((item) => (
<li key={item.id} className="rounded border p-2 text-sm text-gray-700">
{item.title}
</li>
))}
</ul>
)}
{!isSearching && debouncedQuery && results.length === 0 && (
<p className="mt-2 text-sm text-gray-500">該当なし</p>
)}
</div>
);
}
// app/page.tsx
import { SearchBox } from "@/components/SearchBox";
export default function Page() {
return (
<main className="mx-auto max-w-xl p-8">
<h1 className="mb-6 text-2xl font-bold">検索サンプル</h1>
<SearchBox />
</main>
);
}
ポイント
useEffect内でsetTimeoutを設定し、クリーンアップ関数でclearTimeoutすることでデバウンスを実現する- 入力が変わるたびに前のタイマーがキャンセルされるため、最後の入力から
delayms 後にだけ値が更新される query(入力値)とdebouncedQuery(遅延後の値)を分けることで、入力 UI はリアルタイムに、API リクエストはデバウンス後に送れるdelayを引数にすることで呼び出し側で遅延時間を調整できる(一般的には 300〜500ms)useEffectの依存配列にdebouncedQueryを含めることで、値が変化したときだけ検索が走る