Install
$ agentstack add skill-wsimmonds-claude-nextjs-skills-nextjs-use-search-params-suspense ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
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 `` 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)
// app/page.tsx
import { Suspense } from 'react';
import SearchComponent from './SearchComponent';
export default function Page() {
return (
Loading...}>
);
}
// app/SearchComponent.tsx
'use client';
import { useSearchParams } from 'next/navigation';
export default function SearchComponent() {
const searchParams = useSearchParams();
const query = searchParams.get('q') || '';
return (
Search Results for: {query}
);
}
Inline Pattern (Single File)
Sometimes you want everything in one file:
// 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 (
Search: {query}
Results for "{query}"
);
}
export default function Page() {
return (
Loading search...}>
);
}
TypeScript: NEVER Use any Type
// ❌ WRONG
function Component({ params }: any) { ... }
// ✅ CORRECT
// useSearchParams returns ReadonlyURLSearchParams
function Component() {
const searchParams = useSearchParams();
const value: string | null = searchParams.get('key');
}
Real-World Examples
Example 1: Search Interface
// 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 (
Search: {query}
Category: {category}
{/* Display search results */}
{/* ... */}
);
}
export default function SearchPage() {
return (
Loading results...}>
);
}
Example 2: Product Filters
// 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 (
updateFilter('category', e.target.value)}
>
All Categories
Electronics
Clothing
updateFilter('sort', e.target.value)}
>
Name
Price
Rating
{/* Product grid filtered by params */}
);
}
export default function ProductsPage() {
return (
Loading products...}>
);
}
Example 3: Pagination
// 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 (
Blog Posts - Page {page}
{/* Blog posts for current page */}
goToPage(page - 1)}
>
Previous
Page {page}
goToPage(page + 1)}>
Next
);
}
export default function BlogPage() {
return (
Loading posts...}>
);
}
Working with 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 {/* ... */};
}
Updating URL Parameters
'use client';
import { useSearchParams, useRouter } from 'next/navigation';
function Component() {
const searchParams = useSearchParams();
const router = useRouter();
const updateParams = (updates: Record) => {
// 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 (
updateParams({ sort: 'price', order: 'asc' })}>
Sort by Price
);
}
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); // Debounce 300ms
return () => clearTimeout(timer);
}, [query, searchParams, router]);
return (
setQuery(e.target.value)}
placeholder="Search..."
/>
);
}
export default function Page() {
return (
Loading...}>
);
}
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) => {
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 (
updateFilters({ category: e.target.value })}
>
All Categories
Electronics
updateFilters({ priceMin: e.target.value })}
/>
Clear Filters
);
}
export default function Page() {
return (
Loading filters...}>
);
}
Common Mistakes
❌ Mistake 1: Missing 'use client'
// ❌ WRONG - Missing 'use client'
import { useSearchParams } from 'next/navigation';
export default function Page() {
const searchParams = useSearchParams(); // ERROR!
return {searchParams.get('q')};
}
// ✅ CORRECT
'use client'; // Added!
import { useSearchParams } from 'next/navigation';
export default function Page() {
const searchParams = useSearchParams();
return {searchParams.get('q')};
}
❌ Mistake 2: Missing Suspense Wrapper
// ❌ WRONG - Missing Suspense
'use client';
import { useSearchParams } from 'next/navigation';
export default function Page() {
const searchParams = useSearchParams(); // Will cause issues!
return {searchParams.get('q')};
}
// ✅ CORRECT
'use client';
import { Suspense } from 'react';
import { useSearchParams } from 'next/navigation';
function SearchContent() {
const searchParams = useSearchParams();
return {searchParams.get('q')};
}
export default function Page() {
return (
Loading...}>
);
}
❌ Mistake 3: Using in Server Component
// ❌ 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 ...;
}
// ✅ CORRECT - Use searchParams prop in server components
export default async function Page({
searchParams,
}: {
searchParams: Promise;
}) {
const { q } = await searchParams;
return Query: {q};
}
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:
- [ ] Add
'use client'directive at top of file - [ ] Import
Suspensefrom 'react' - [ ] Import
useSearchParamsfrom 'next/navigation' - [ ] Wrap component using
useSearchParamsin `` - [ ] Provide a fallback to Suspense
- [ ] Call
useSearchParams()inside wrapped component - [ ] Use
.get(),.has(), or.getAll()to read params
Summary
useSearchParams with Suspense:
- ✅ Requires
'use client'directive - ✅ Requires `` wrapper
- ✅ Use for client-side URL param reading
- ✅ Combine with
useRouter()for updating params - ✅ Best for filters, search, pagination
- ❌ NOT for server components (use
searchParamsprop instead)
This is the recommended pattern for client-side URL parameter handling in Next.js App Router.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: wsimmonds
- Source: wsimmonds/claude-nextjs-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.