レビュー済み·難易度: 初級·更新: 2026-04-18

Next.js で URL クエリパラメータのフィルタを一括リセットする

複数の URL クエリパラメータ(q・category・status など)をまとめて削除してフィルタ状態を初期化するパターン。リセットボタンと個別削除の両方を実装する例。

nextjssearch-filterrouting

対応バージョン

nextjs 15react 19

前提環境

Next.js App Router の useSearchParams と useRouter の基本を理解していること

概要

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() が空文字になるケースでは ? を付けずにパス名のみを渡す
  • activeFiltersuseSearchParams から導出することで、URL が変わるたびに自動的に再レンダリングされる。外部 state なしで URL を単一ソースとして扱える
  • searchParams.size で現在のパラメータ数を確認できる(Next.js 15 / Web API の URLSearchParams)。0 のときはリセットボタン自体を非表示にしてノイズを減らす

注意点

nextjs-api-sort は Route Handler 側のソート。nextjs-api-search は API 検索。react-multi-filter は複数条件フィルタ UI。これは URL に乗った複合フィルタ条件を一括またはパラメータ単位でリセットするクライアント操作パターンに特化。

関連サンプル