nextjs-use-search-params-suspense
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseNext.js: useSearchParams with Suspense Pattern
Next.js:结合Suspense使用useSearchParams的模式
Pattern Overview
模式概述
The useSearchParams hook requires TWO things:
- Component must have directive
'use client' - Component must be wrapped in a boundary
<Suspense>
This is a Next.js requirement, not optional!
使用useSearchParams钩子需要满足两个条件:
- 组件必须包含指令
'use client' - 组件必须被边界包裹
<Suspense>
这是Next.js的强制要求,并非可选!
Why This Pattern?
为什么要使用这种模式?
useSearchParams reads URL query parameters:
- →
/search?q=shoesreturnssearchParams.get('q')"shoes" - → Read multiple params
/products?category=electronics&sort=price
Why Suspense?
Next.js uses React 18's Suspense to handle the async nature of reading URL params during server-side rendering and hydration.
useSearchParams用于读取URL查询参数:
- →
/search?q=shoes返回searchParams.get('q')"shoes" - → 读取多个参数
/products?category=electronics&sort=price
为什么需要Suspense?
Next.js使用React 18的Suspense来处理在服务端渲染和水合过程中读取URL参数的异步特性。
The Pattern
实现模式
Single-File Pattern (Recommended)
单文件模式(推荐)
typescript
// 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>
);
}typescript
// 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>
);
}Inline Pattern (Single File)
内联模式(单文件)
Sometimes you want everything in one file:
typescript
// 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
// 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: NEVER Use any
Type
anyTypeScript:切勿使用any
类型
anytypescript
// ❌ WRONG
function Component({ params }: any) { ... }
// ✅ CORRECT
// useSearchParams returns ReadonlyURLSearchParams
function Component() {
const searchParams = useSearchParams();
const value: string | null = searchParams.get('key');
}typescript
// ❌ 错误写法
function Component({ params }: any) { ... }
// ✅ 正确写法
// useSearchParams返回ReadonlyURLSearchParams类型
function Component() {
const searchParams = useSearchParams();
const value: string | null = searchParams.get('key');
}Real-World Examples
实际应用示例
Example 1: Search Interface
示例1:搜索界面
typescript
// 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>
);
}typescript
// 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>
{/* 展示搜索结果 */}
<div className="results">
{/* ... */}
</div>
</div>
);
}
export default function SearchPage() {
return (
<div>
<Suspense fallback={<div>Loading results...</div>}>
<SearchResults />
</Suspense>
</div>
);
}Example 2: Product Filters
示例2:产品筛选器
typescript
// 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>
);
}typescript
// 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">所有分类</option>
<option value="electronics">电子产品</option>
<option value="clothing">服饰</option>
</select>
<select
value={sort}
onChange={(e) => updateFilter('sort', e.target.value)}
>
<option value="name">按名称排序</option>
<option value="price">按价格排序</option>
<option value="rating">按评分排序</option>
</select>
</div>
<div className="products">
{/* 根据参数筛选后的产品网格 */}
</div>
</div>
);
}
export default function ProductsPage() {
return (
<Suspense fallback={<div>Loading products...</div>}>
<ProductList />
</Suspense>
);
}Example 3: Pagination
示例3:分页功能
typescript
// 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>
);
}typescript
// 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>博客文章 - 第 {page} 页</h1>
<div className="posts">
{/* 当前页的博客文章 */}
</div>
<div className="pagination">
<button
disabled={page === 1}
onClick={() => goToPage(page - 1)}
>
上一页
</button>
<span>第 {page} 页</span>
<button onClick={() => goToPage(page + 1)}>
下一页
</button>
</div>
</div>
);
}
export default function BlogPage() {
return (
<Suspense fallback={<div>Loading posts...</div>}>
<BlogPosts />
</Suspense>
);
}Working with URLSearchParams
URLSearchParams的常用操作
typescript
'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>;
}typescript
'use client';
import { useSearchParams } from 'next/navigation';
function Component() {
const searchParams = useSearchParams();
// 获取单个参数值
const query = searchParams.get('q'); // string | null
const category = searchParams.get('category'); // string | null
// 获取一个键对应的所有值(适用于多选场景)
const tags = searchParams.getAll('tag'); // string[]
// 检查键是否存在
const hasSort = searchParams.has('sort'); // boolean
// 遍历所有参数
searchParams.forEach((value, key) => {
console.log(`${key}: ${value}`);
});
// 转换为普通对象
const paramsObject = Object.fromEntries(searchParams.entries());
return <div>{/* ... */}</div>;
}Updating URL Parameters
更新URL参数
typescript
'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>
);
}typescript
'use client';
import { useSearchParams, useRouter } from 'next/navigation';
function Component() {
const searchParams = useSearchParams();
const router = useRouter();
const updateParams = (updates: Record<string, string>) => {
// 基于当前参数创建新的URLSearchParams实例
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' })}>
按价格排序
</button>
);
}Common Patterns
常见扩展模式
Pattern: Search with Debounce
模式:带防抖的搜索
typescript
'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>
);
}typescript
'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); // 300ms防抖
return () => clearTimeout(timer);
}, [query, searchParams, router]);
return (
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="搜索..."
/>
);
}
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<SearchInput />
</Suspense>
);
}Pattern: Multiple Filters
模式:多条件筛选
typescript
'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>
);
}typescript
'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); // 移除所有参数
};
return (
<div className="filters">
<select
value={currentFilters.category || ''}
onChange={(e) => updateFilters({ category: e.target.value })}
>
<option value="">所有分类</option>
<option value="electronics">电子产品</option>
</select>
<input
type="number"
placeholder="最低价格"
value={currentFilters.priceMin || ''}
onChange={(e) => updateFilters({ priceMin: e.target.value })}
/>
<button onClick={clearFilters}>清除筛选</button>
</div>
);
}
export default function Page() {
return (
<Suspense fallback={<div>Loading filters...</div>}>
<FilterPanel />
</Suspense>
);
}Common Mistakes
常见错误
❌ Mistake 1: Missing 'use client'
❌ 错误1:缺少'use client'指令
typescript
// ❌ WRONG - Missing 'use client'
import { useSearchParams } from 'next/navigation';
export default function Page() {
const searchParams = useSearchParams(); // ERROR!
return <div>{searchParams.get('q')}</div>;
}typescript
// ✅ CORRECT
'use client'; // Added!
import { useSearchParams } from 'next/navigation';
export default function Page() {
const searchParams = useSearchParams();
return <div>{searchParams.get('q')}</div>;
}typescript
// ❌ 错误写法 - 缺少'use client'
import { useSearchParams } from 'next/navigation';
export default function Page() {
const searchParams = useSearchParams(); // 报错!
return <div>{searchParams.get('q')}</div>;
}typescript
// ✅ 正确写法
'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
❌ 错误2:缺少Suspense包装
typescript
// ❌ 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>;
}typescript
// ✅ 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>
);
}typescript
// ❌ 错误写法 - 缺少Suspense
'use client';
import { useSearchParams } from 'next/navigation';
export default function Page() {
const searchParams = useSearchParams(); // 会导致问题!
return <div>{searchParams.get('q')}</div>;
}typescript
// ✅ 正确写法
'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>加载中...</div>}>
<SearchContent />
</Suspense>
);
}❌ Mistake 3: Using in Server Component
❌ 错误3:在服务端组件中使用
typescript
// ❌ 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>;
}typescript
// ✅ 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>;
}typescript
// ❌ 错误写法 - 尝试在服务端组件中使用
import { useSearchParams } from 'next/navigation';
export default async function Page() { // async表示是服务端组件
const searchParams = useSearchParams(); // 报错!钩子无法在服务端组件中使用
return <div>...</div>;
}typescript
// ✅ 正确写法 - 在服务端组件中使用searchParams属性
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ q?: string }>;
}) {
const { q } = await searchParams;
return <div>查询词:{q}</div>;
}Server vs Client searchParams
服务端与客户端searchParams对比
| Feature | Server Component | Client Component |
|---|---|---|
| Access method | | |
| 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 |
| 特性 | 服务端组件 | 客户端组件 |
|---|---|---|
| 访问方式 | | |
| 是否需要'use client' | ❌ 不需要 | ✅ 需要 |
| 是否需要Suspense | ❌ 不需要 | ✅ 需要 |
| 是否支持异步 | ✅ 支持 | ❌ 不支持 |
| 是否可更新参数 | ❌ 不支持(使用Link/redirect) | ✅ 支持(使用router.push) |
| 适用场景 | 初始加载、SEO优化 | 动态筛选、实时更新 |
Quick Checklist
快速检查清单
When using useSearchParams:
- Add directive at top of file
'use client' - Import from 'react'
Suspense - Import from 'next/navigation'
useSearchParams - Wrap component using in
useSearchParams<Suspense> - Provide a fallback to Suspense
- Call inside wrapped component
useSearchParams() - Use ,
.get(), or.has()to read params.getAll()
使用useSearchParams时:
- 在文件顶部添加指令
'use client' - 从'react'中导入
Suspense - 从'next/navigation'中导入
useSearchParams - 将使用的组件用
useSearchParams包裹<Suspense> - 为Suspense提供fallback内容
- 在被包裹的组件内部调用
useSearchParams() - 使用、
.get()或.has()读取参数.getAll()
Summary
总结
useSearchParams with Suspense:
- ✅ Requires directive
'use client' - ✅ Requires wrapper
<Suspense> - ✅ Use for client-side URL param reading
- ✅ Combine with for updating params
useRouter() - ✅ Best for filters, search, pagination
- ❌ NOT for server components (use prop instead)
searchParams
This is the recommended pattern for client-side URL parameter handling in Next.js App Router.
结合Suspense使用useSearchParams:
- ✅ 必须包含指令
'use client' - ✅ 必须被包裹
<Suspense> - ✅ 用于客户端侧的URL参数读取
- ✅ 可与结合更新参数
useRouter() - ✅ 最适用于筛选器、搜索、分页功能
- ❌ 不可在服务端组件中使用(改用属性)
searchParams
这是Next.js App Router中处理客户端URL参数的推荐模式。