概要
useSearchParams で現在のフィルタ条件を読み取り、useRouter で URL を更新してフィルタをリセットする。一括リセット(全パラメータ削除)とパラメータ単位の削除を実装し、フィルターチップ UI と組み合わせて使いやすいリセット導線を作る。
インストール
# 追加インストールは不要
実装
フィルタリセットフック
// hooks/useFilterReset.ts
"use client";
import { useCallback } from "react";
import { useRouter, useSearchParams } from "next/navigation";
const FILTER_KEYS = ["q", "category", "status", "sort", "order"] as const;
type FilterKey = (typeof FILTER_KEYS)[number];
export function useFilterReset() {
const router = useRouter();
const searchParams = useSearchParams();
// アクティブなフィルタキーと値のマップを返す
const activeFilters = Object.fromEntries(
FILTER_KEYS.filter((key) => searchParams.has(key)).map((key) => [
key,
searchParams.get(key) ?? "",
]),
) as Partial<Record<FilterKey, string>>;
const hasActiveFilters = Object.keys(activeFilters).length > 0;
// 単一パラメータを削除してURLを更新
const removeFilter = useCallback(
(key: FilterKey) => {
const params = new URLSearchParams(searchParams.toString());
params.delete(key);
const qs = params.toString();
router.push(qs ? `?${qs}` : window.location.pathname);
},
[router, searchParams],
);
// 全フィルタを一括削除してURLをリセット
const resetAllFilters = useCallback(() => {
router.push(window.location.pathname);
}, [router]);
return { activeFilters, hasActiveFilters, removeFilter, resetAllFilters };
}
フィルタリセットバーコンポーネント
// components/FilterResetBar.tsx
"use client";
import { Suspense } from "react";
import { useFilterReset } from "@/hooks/useFilterReset";
const FILTER_LABELS: Record<string, string> = {
q: "キーワード",
category: "カテゴリ",
status: "ステータス",
sort: "並び順",
order: "順序",
};
function FilterResetBarInner() {
const { activeFilters, hasActiveFilters, removeFilter, resetAllFilters } = useFilterReset();
if (!hasActiveFilters) return null;
return (
<div className="flex flex-wrap items-center gap-2 rounded-lg bg-gray-50 px-4 py-2">
<span className="text-xs text-gray-500">適用中:</span>
{Object.entries(activeFilters).map(([key, value]) => (
<span
key={key}
className="flex items-center gap-1 rounded-full bg-white px-3 py-1 text-xs font-medium text-gray-700 shadow-sm ring-1 ring-gray-200"
>
<span className="text-gray-400">{FILTER_LABELS[key] ?? key}:</span>
<span>{value}</span>
<button
onClick={() => removeFilter(key as "q" | "category" | "status" | "sort" | "order")}
className="ml-1 rounded-full text-gray-400 hover:text-gray-600"
aria-label={`${FILTER_LABELS[key] ?? key} フィルタを削除`}
>
✕
</button>
</span>
))}
<button
onClick={resetAllFilters}
className="ml-auto text-xs text-blue-600 hover:underline"
>
すべてクリア
</button>
</div>
);
}
export default function FilterResetBar() {
return (
<Suspense fallback={null}>
<FilterResetBarInner />
</Suspense>
);
}
一覧ページへの組み込み
// app/samples/page.tsx
import { Suspense } from "react";
import FilterResetBar from "@/components/FilterResetBar";
import SampleList from "@/components/SampleList";
export default function SamplesPage() {
return (
<main className="mx-auto max-w-4xl px-4 py-8">
<h1 className="mb-6 text-2xl font-bold">サンプル一覧</h1>
{/* 検索フォームなど */}
<div className="mb-4 space-y-3">
{/* ... フィルタ UI ... */}
<FilterResetBar />
</div>
<Suspense fallback={<p className="text-gray-400">読み込み中…</p>}>
<SampleList />
</Suspense>
</main>
);
}
リセットボタン単体(シンプル版)
// components/ResetFiltersButton.tsx
"use client";
import { Suspense } from "react";
import { useRouter, useSearchParams } from "next/navigation";
function ResetButtonInner() {
const router = useRouter();
const searchParams = useSearchParams();
const hasFilters = searchParams.size > 0;
if (!hasFilters) return null;
return (
<button
onClick={() => router.push(window.location.pathname)}
className="rounded border border-gray-300 px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-50"
>
フィルタをリセット
</button>
);
}
export default function ResetFiltersButton() {
return (
<Suspense fallback={null}>
<ResetButtonInner />
</Suspense>
);
}
ポイント
router.push(window.location.pathname)でクエリパラメータなしの現在パスに遷移し、全フィルタを一括リセットする。ハードコードせずに動的に現在のパスを使うため、どのページでも再利用できるuseSearchParamsを使うコンポーネントは<Suspense>でラップする必要がある。内側に実装コンポーネントを分けてfallback={null}を渡すことで、SSR 時のビルドエラーを防ぎながら自然な表示を維持する- 単一パラメータの削除は
URLSearchParams.delete(key)→router.pushで実現する。toString()が空文字になるケースでは?を付けずにパス名のみを渡す activeFiltersをuseSearchParamsから導出することで、URL が変わるたびに自動的に再レンダリングされる。外部 state なしで URL を単一ソースとして扱えるsearchParams.sizeで現在のパラメータ数を確認できる(Next.js 15 / Web API のURLSearchParams)。0 のときはリセットボタン自体を非表示にしてノイズを減らす