概要
Next.js App Router では、URL のクエリパラメータ(?category=api)をサーバーコンポーネントの searchParams で受け取り、サーバー側でフィルタリングできる。
クライアントコンポーネントから useRouter でURLを更新すると、サーバーコンポーネントが再実行されてフィルタ結果が反映される。
インストール
# 追加インストールは不要
実装例
// src/app/samples/page.tsx(サーバーコンポーネント)
type Post = { id: number; title: string; category: string };
type Props = {
searchParams: Promise<{ category?: string; q?: string }>;
};
async function getPosts(): Promise<Post[]> {
// 実際の実装では DB や API から取得する
return [
{ id: 1, title: "useQuery でデータフェッチ", category: "api" },
{ id: 2, title: "useReducer で状態管理", category: "state-management" },
{ id: 3, title: "Zod でバリデーション", category: "validation" },
{ id: 4, title: "useMutation で POST", category: "api" },
];
}
export default async function SamplesPage({ searchParams }: Props) {
const { category, q } = await searchParams;
const posts = await getPosts();
// サーバー側でフィルタリング
const filtered = posts.filter((post) => {
const matchCategory = !category || post.category === category;
const matchQuery = !q || post.title.toLowerCase().includes(q.toLowerCase());
return matchCategory && matchQuery;
});
return (
<div className="p-6 space-y-4">
{/* クライアントコンポーネントのフィルタ UI */}
<FilterBar currentCategory={category} currentQuery={q} />
{/* フィルタ結果 */}
<p className="text-sm text-gray-500">{filtered.length} 件</p>
<ul className="space-y-2">
{filtered.map((post) => (
<li key={post.id} className="rounded border px-4 py-3 text-sm">
<p className="font-medium">{post.title}</p>
<p className="text-xs text-gray-400">{post.category}</p>
</li>
))}
</ul>
</div>
);
}
// src/app/samples/FilterBar.tsx(クライアントコンポーネント)
"use client";
import { useRouter, usePathname } from "next/navigation";
import { useCallback } from "react";
type Props = { currentCategory?: string; currentQuery?: string };
const CATEGORIES = ["api", "state-management", "validation", "form"];
export function FilterBar({ currentCategory, currentQuery }: Props) {
const router = useRouter();
const pathname = usePathname();
const updateParams = useCallback(
(key: string, value: string) => {
const params = new URLSearchParams(window.location.search);
if (value) {
params.set(key, value);
} else {
params.delete(key);
}
router.push(`${pathname}?${params.toString()}`);
},
[router, pathname]
);
return (
<div className="flex flex-wrap gap-2">
{/* カテゴリフィルタ */}
<div className="flex gap-1">
<button
onClick={() => updateParams("category", "")}
className={`rounded border px-3 py-1 text-xs ${!currentCategory ? "bg-gray-900 text-white" : ""}`}
>
すべて
</button>
{CATEGORIES.map((cat) => (
<button
key={cat}
onClick={() => updateParams("category", cat)}
className={`rounded border px-3 py-1 text-xs ${currentCategory === cat ? "bg-gray-900 text-white" : ""}`}
>
{cat}
</button>
))}
</div>
{/* キーワード検索 */}
<input
defaultValue={currentQuery}
onChange={(e) => updateParams("q", e.target.value)}
placeholder="キーワード検索"
className="rounded border px-3 py-1 text-sm"
/>
</div>
);
}
ポイント
- サーバーコンポーネントは
searchParamsprops でクエリを受け取る(Next.js 15 ではawaitが必要) URLSearchParamsを使うと複数のクエリパラメータを維持しながら1つだけ更新できるrouter.pushで URL を変更するとサーバーコンポーネントが再レンダリングされ、フィルタ結果が自動更新される- URL にフィルタ状態が入るため、リロードや共有リンクでフィルタ状態が保持される
- フィルタ処理はサーバー側で行うため、大量データでも DB クエリで効率的に絞り込める