nextjs-use-search-params-suspense について
useSearchParams で URL クエリパラメーターを読む Next.js App Router 開発者向けのパターンガイドです。コンポーネントを Suspense で囲む方法と、クライアントフックとサーバー側の searchParams プロパティの違いを説明します。
主な機能
- 01クライアントコンポーネントでのクエリパラメーター取得
- 02フォールバック付き Suspense 境界の使用
- 03get、has、getAll によるパラメーター参照
- 04useRouter による URL パラメーター更新
- 05サーバーコンポーネントでの searchParams の利用方法
活用シーン
- 01URL に基づく検索ページの作成
- 02商品フィルターの取得と更新
- 03ページネーション用クエリの実装
- 04クライアント側でのリアルタイムなパラメーター更新
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ wsimmonds/ claude-nextjs-skills --skill nextjs-use-search-params-suspenseインストール後、Claude Code で/nextjs-use-search-params-suspenseを入力して読み込まれたか確認します。
詳細
Next.js:Suspense パターンでの useSearchParams
パターンの概要
useSearchParams フックには、次の 2 つが必要です:
- コンポーネントに
'use client'ディレクティブを指定する - コンポーネントを
<Suspense>境界でラップする
これは Next.js の必須要件であり、省略できません!
このパターンを使う理由
useSearchParams は URL のクエリパラメーターを読み取ります:
/search?q=shoes→searchParams.get('q')は"shoes"を返します/products?category=electronics&sort=price→ 複数のパラメーターを読み取ります
なぜ Suspense が必要なのでしょうか? Next.js は React 18 の Suspense を使い、サーバーサイドレンダリングとハイドレーション中に URL パラメーターを読み取る際の非同期処理に対応します。
パターン
単一ファイルパターン(推奨)
// app/page.tsx
import { Suspense } from 'react';
import SearchComponent from './SearchComponent';
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<SearchComponent />
</Suspense>
);
}
// app/SearchComponent.tsx
'use client';
import { useSearchParams } from 'next/navigation';
export default function SearchComponent() {
const searchParams = useSearchParams();
const query = searchParams.get('q') || '';
return (
<div>
<h1>Search Results for: {query}</h1>
</div>
);
}
インラインパターン(単一ファイル)
すべてを 1 つのファイルにまとめたい場合があります:
// app/page.tsx
'use client';
import { Suspense } from 'react';
import { useSearchParams } from 'next/navigation';
function SearchContent() {
const searchParams = useSearchParams();
const query = searchParams.get('q') || '';
return (
<div>
<h1>Search: {query}</h1>
<p>Results for "{query}"</p>
</div>
);
}
export default function Page() {
return (
<Suspense fallback={<div>Loading search...</div>}>
<SearchContent />
</Suspense>
);
}
TypeScript:any 型は絶対に使わない
// ❌ WRONG
function Component({ params }: any) { ... }
// ✅ CORRECT
// useSearchParams returns ReadonlyURLSearchParams
function Component() {
const searchParams = useSearchParams();
const value: string | null = searchParams.get('key');
}
実際の使用例
例 1:検索インターフェース
// app/search/page.tsx
'use client';
import { Suspense } from 'react';
import { useSearchParams } from 'next/navigation';
function SearchResults() {
const searchParams = useSearchParams();
const query = searchParams.get('q') || '';
const category = searchParams.get('category') || 'all';
return (
<div>
<h1>Search: {query}</h1>
<p>Category: {category}</p>
{/* Display search results */}
<div className="results">
{/* ... */}
</div>
</div>
);
}
export default function SearchPage() {
return (
<div>
<Suspense fallback={<div>Loading results...</div>}>
<SearchResults />
</Suspense>
</div>
);
}
例 2:商品フィルター
// app/products/page.tsx
'use client';
import { Suspense } from 'react';
import { useSearchParams, useRouter } from 'next/navigation';
function ProductList() {
const searchParams = useSearchParams();
const router = useRouter();
const category = searchParams.get('category') || 'all';
const sort = searchParams.get('sort') || 'name';
const minPrice = searchParams.get('minPrice') || '0';
const updateFilter = (key: string, value: string) => {
const params = new URLSearchParams(searchParams.toString());
params.set(key, value);
router.push(`?${params.toString()}`);
};
return (
<div>
<div className="filters">
<select
value={category}
onChange={(e) => updateFilter('category', e.target.value)}
>
<option value="all">All Categories</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
</select>
<select
value={sort}
onChange={(e) => updateFilter('sort', e.target.value)}
>
<option value="name">Name</option>
<option value="price">Price</option>
<option value="rating">Rating</option>
</select>
</div>
<div className="products">
{/* Product grid filtered by params */}
</div>
</div>
);
}
export default function ProductsPage() {
return (
<Suspense fallback={<div>Loading products...</div>}>
<ProductList />
</Suspense>
);
}
例 3:ページネーション
// app/blog/page.tsx
'use client';
import { Suspense } from 'react';
import { useSearchParams, useRouter } from 'next/navigation';
function BlogPosts() {
const searchParams = useSearchParams();
const router = useRouter();
const page = parseInt(searchParams.get('page') || '1', 10);
const perPage = 10;
const goToPage = (newPage: number) => {
const params = new URLSearchParams(searchParams.toString());
params.set('page', newPage.toString());
router.push(`?${params.toString()}`);
};
return (
<div>
<h1>Blog Posts - Page {page}</h1>
<div className="posts">
{/* Blog posts for current page */}
</div>
<div className="pagination">
<button
disabled={page === 1}
onClick={() => goToPage(page - 1)}
>
Previous
</button>
<span>Page {page}</span>
<button onClick={() => goToPage(page + 1)}>
Next
</button>
</div>
</div>
);
}
export default function BlogPage() {
return (
<Suspense fallback={<div>Loading posts...</div>}>
<BlogPosts />
</Suspense>
);
}
URLSearchParams の使用
'use client';
import { useSearchParams } from 'next/navigation';
function Component() {
const searchParams = useSearchParams();
// Get single value
const query = searchParams.get('q'); // string | null
const category = searchParams.get('category'); // string | null
// Get all values for a key (for multi-select)
const tags = searchParams.getAll('tag'); // string[]
// Check if key exists
const hasSort = searchParams.has('sort'); // boolean
// Iterate over all params
searchParams.forEach((value, key) => {
console.log(`${key}: ${value}`);
});
// Convert to regular object
const paramsObject = Object.fromEntries(searchParams.entries());
return <div>{/* ... */}</div>;
}
URL パラメーターの更新
'use client';
import { useSearchParams, useRouter } from 'next/navigation';
function Component() {
const searchParams = useSearchParams();
const router = useRouter();
const updateParams = (updates: Record<string, string>) => {
// Create new URLSearchParams from current params
const params = new URLSearchParams(searchParams.toString());
// Apply updates
Object.entries(updates).forEach(([key, value]) => {
if (value) {
params.set(key, value);
} else {
params.delete(key); // Remove if value is empty
}
});
// Navigate with new params
router.push(`?${params.toString()}`);
};
return (
<button onClick={() => updateParams({ sort: 'price', order: 'asc' })}>
Sort by Price
</button>
);
}
よく使われるパターン
パターン:デバウンス付き検索
'use client';
import { Suspense, useState, useEffect } from 'react';
import { useSearchParams, useRouter } from 'next/navigation';
function SearchInput() {
const searchParams = useSearchParams();
const router = useRouter();
const [query, setQuery] = useState(searchParams.get('q') || '');
useEffect(() => {
const timer = setTimeout(() => {
const params = new URLSearchParams(searchParams.toString());
if (query) {
params.set('q', query);
} else {
params.delete('q');
}
router.push(`?${params.toString()}`);
}, 300); // Debounce 300ms
return () => clearTimeout(timer);
}, [query, searchParams, router]);
return (
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
);
}
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<SearchInput />
</Suspense>
);
}
パターン:複数のフィルター
'use client';
import { Suspense } from 'react';
import { useSearchParams, useRouter } from 'next/navigation';
interface Filters {
category?: string;
priceMin?: string;
priceMax?: string;
inStock?: string;
}
function FilterPanel() {
const searchParams = useSearchParams();
const router = useRouter();
const currentFilters: Filters = {
category: searchParams.get('category') || undefined,
priceMin: searchParams.get('priceMin') || undefined,
priceMax: searchParams.get('priceMax') || undefined,
inStock: searchParams.get('inStock') || undefined,
};
const updateFilters = (newFilters: Partial<Filters>) => {
const params = new URLSearchParams(searchParams.toString());
Object.entries({ ...currentFilters, ...newFilters }).forEach(
([key, value]) => {
if (value) {
params.set(key, value);
} else {
params.delete(key);
}
}
);
router.push(`?${params.toString()}`);
};
const clearFilters = () => {
router.push(window.location.pathname); // Remove all params
};
return (
<div className="filters">
<select
value={currentFilters.category || ''}
onChange={(e) => updateFilters({ category: e.target.value })}
>
<option value="">All Categories</option>
<option value="electronics">Electronics</option>
</select>
<input
type="number"
placeholder="Min Price"
value={currentFilters.priceMin || ''}
onChange={(e) => updateFilters({ priceMin: e.target.value })}
/>
<button onClick={clearFilters}>Clear Filters</button>
</div>
);
}
export default function Page() {
return (
<Suspense fallback={<div>Loading filters...</div>}>
<FilterPanel />
</Suspense>
);
}
よくある間違い
❌ 間違い 1:'use client' がない
// ❌ WRONG - Missing 'use client'
import { useSearchParams } from 'next/navigation';
export default function Page() {
const searchParams = useSearchParams(); // ERROR!
return <div>{searchParams.get('q')}</div>;
}
// ✅ CORRECT
'use client'; // Added!
import { useSearchParams } from 'next/navigation';
export default function Page() {
const searchParams = useSearchParams();
return <div>{searchParams.get('q')}</div>;
}
❌ 間違い 2:Suspense ラッパーがない
// ❌ WRONG - Missing Suspense
'use client';
import { useSearchParams } from 'next/navigation';
export default function Page() {
const searchParams = useSearchParams(); // Will cause issues!
return <div>{searchParams.get('q')}</div>;
}
// ✅ CORRECT
'use client';
import { Suspense } from 'react';
import { useSearchParams } from 'next/navigation';
function SearchContent() {
const searchParams = useSearchParams();
return <div>{searchParams.get('q')}</div>;
}
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<SearchContent />
</Suspense>
);
}
❌ 間違い 3:サーバーコンポーネントで使用する
// ❌ WRONG - Trying to use in server component
import { useSearchParams } from 'next/navigation';
export default async function Page() { // async = server component
const searchParams = useSearchParams(); // ERROR! Hooks don't work in server components
return <div>...</div>;
}
// ✅ CORRECT - Use searchParams prop in server components
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ q?: string }>;
}) {
const { q } = await searchParams;
return <div>Query: {q}</div>;
}
サーバーとクライアントの searchParams
| 機能 | サーバーコンポーネント | クライアントコンポーネント |
|---|---|---|
| アクセス方法 | searchParams プロパティ | useSearchParams() フック |
| 'use client' が必要 | ❌ いいえ | ✅ はい |
| Suspense が必要 | ❌ いいえ | ✅ はい |
| 非同期にできる | ✅ はい | ❌ いいえ |
| パラメーターを更新できる | ❌ いいえ(Link/redirect を使用) | ✅ はい(router.push を使用) |
| 最適な用途 | 初回読み込み、SEO | 動的なフィルター、リアルタイム更新 |
クイックチェックリスト
useSearchParams を使用する場合:
- ファイルの先頭に
'use client'ディレクティブを追加する - 'react' から
Suspenseをインポートする - 'next/navigation' から
useSearchParamsをインポートする -
useSearchParamsを使用するコンポーネントを<Suspense>でラップする - Suspense にフォールバックを指定する
- ラップしたコンポーネント内で
useSearchParams()を呼び出す - パラメーターの読み取りには
.get()、.has()、または.getAll()を使う
まとめ
Suspense と組み合わせた useSearchParams:
- ✅
'use client'ディレクティブが必要です - ✅
<Suspense>ラッパーが必要です - ✅ クライアント側で URL パラメーターを読み取る際に使用します
- ✅ パラメーターの更新には
useRouter()と組み合わせます - ✅ フィルター、検索、ページネーションに最適です
- ❌ サーバーコンポーネントでは使用しません(代わりに
searchParamsプロパティを使用します)
Next.js App Router でクライアント側の URL パラメーターを扱う場合は、このパターンを推奨します。
コメント
Markdown 対応