| name | nextjs-use-search-params-suspense |
| description | Pattern for using useSearchParams hook with Suspense boundary in Next.js. Covers the required combination of 'use client' directive and Suspense wrapper when accessing URL query parameters in client components. Use when building search interfaces, filters, pagination, or any feature that needs to read/manipulate URL query parameters client-side. |
| allowed-tools | Read, Write, Edit, Glob, Grep, Bash |
Next.js: useSearchParams with Suspense Pattern
Pattern Overview
The useSearchParams hook requires TWO things:
- Component must have
'use client' directive
- Component must be wrapped in a
<Suspense> boundary
This is a Next.js requirement, not optional!
Why This Pattern?
useSearchParams reads URL query parameters:
/search?q=shoes → searchParams.get('q') returns "shoes"
/products?category=electronics&sort=price → Read multiple params
Why Suspense?
Next.js uses React 18's Suspense to handle the async nature of reading URL params during server-side rendering and hydration.
The Pattern
Single-File Pattern (Recommended)
import { Suspense } from 'react';
import SearchComponent from './SearchComponent';
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<SearchComponent />
</Suspense>
);
}
'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>
);
}
Inline Pattern (Single File)
Sometimes you want everything in one file:
'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: NEVER Use any Type
function Component({ params }: any) { ... }
function Component() {
const searchParams = useSearchParams();
const value: string | null = searchParams.get('key');
}
Real-World Examples
Example 1: Search Interface
'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...}>
);
}
Example 2: Product Filters
'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">
<
=
= => updateFilter('category', e.target.value)}
>
All Categories
Electronics
Clothing
updateFilter('sort', e.target.value)}
>
Name
Price
Rating
{/* Product grid filtered by params */}
);
}
() {
(
);
}
Example 3: Pagination
'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>
goToPage(page - 1)}
>
Previous
Page {page}
goToPage(page + 1)}>
Next
);
}
() {
(
);
}
Working with URLSearchParams
'use client';
import { useSearchParams } from 'next/navigation';
function Component() {
const searchParams = useSearchParams();
const query = searchParams.get('q');
const category = searchParams.get('category');
const tags = searchParams.getAll('tag');
const hasSort = searchParams.has('sort');
searchParams.forEach((value, key) => {
console.log(`${key}: ${value}`);
});
const paramsObject = Object.fromEntries(searchParams.entries());
return <div>{/* ... */}</div>;
}
Updating URL Parameters
'use client';
import { useSearchParams, useRouter } from 'next/navigation';
function Component() {
const searchParams = useSearchParams();
const router = useRouter();
const updateParams = (updates: Record<string, string>) => {
const params = new URLSearchParams(searchParams.toString());
Object.entries(updates).forEach(([key, value]) => {
if (value) {
params.set(key, value);
} else {
params.delete(key);
}
});
router.push(`?${params.toString()}`);
};
return (
<button onClick={() => updateParams({ sort: 'price', order: 'asc' })}>
Sort by Price
</button>
);
}
Common Patterns
Pattern: Search with Debounce
'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);
return () => clearTimeout(timer);
}, [query, searchParams, router]);
return (
<input
type="search"
value=
= => setQuery(e.target.value)}
placeholder="Search..."
/>
);
}
() {
(
);
}
Pattern: Multiple Filters
'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 = (searchParams.());
.({ ...currentFilters, ...newFilters }).(
{
(value) {
params.(key, value);
} {
params.(key);
}
}
);
router.();
};
= () => {
router.(..);
};
(
);
}
() {
(
);
}
Common Mistakes
❌ Mistake 1: Missing 'use client'
import { useSearchParams } from 'next/navigation';
export default function Page() {
const searchParams = useSearchParams();
return <div>{searchParams.get('q')}</div>;
}
'use client';
import { useSearchParams } from 'next/navigation';
export default function Page() {
const searchParams = useSearchParams();
return <div>{searchParams.get('q')}</div>;
}
❌ Mistake 2: Missing Suspense Wrapper
'use client';
import { useSearchParams } from 'next/navigation';
export default function Page() {
const searchParams = useSearchParams();
return <div>{searchParams.get('q')}</div>;
}
'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>
);
}
❌ Mistake 3: Using in Server Component
import { useSearchParams } from 'next/navigation';
export default async function Page() {
const searchParams = useSearchParams();
return <div>...</div>;
}
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ q?: string }>;
}) {
const { q } = await searchParams;
return <div>Query: {q}</div>;
}
Server vs Client searchParams
| Feature | Server Component | Client Component |
|---|
| Access method | searchParams prop | useSearchParams() hook |
| Requires 'use client' | ❌ No | ✅ Yes |
| Requires Suspense | ❌ No | ✅ Yes |
| Can be async | ✅ Yes | ❌ No |
| Can update params | ❌ No (use Link/redirect) | ✅ Yes (use router.push) |
| Best for | Initial load, SEO | Dynamic filters, real-time updates |
Quick Checklist
When using useSearchParams:
Summary
useSearchParams with Suspense:
- ✅ Requires
'use client' directive
- ✅ Requires
<Suspense> wrapper
- ✅ Use for client-side URL param reading
- ✅ Combine with
useRouter() for updating params
- ✅ Best for filters, search, pagination
- ❌ NOT for server components (use
searchParams prop instead)
This is the recommended pattern for client-side URL parameter handling in Next.js App Router.