nextjs-use-search-params-suspense

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Next.js: useSearchParams with Suspense Pattern

Next.js:结合Suspense使用useSearchParams的模式

Pattern Overview

模式概述

The useSearchParams hook requires TWO things:
  1. Component must have
    'use client'
    directive
  2. Component must be wrapped in a
    <Suspense>
    boundary
This is a Next.js requirement, not optional!
使用useSearchParams钩子需要满足两个条件:
  1. 组件必须包含
    'use client'
    指令
  2. 组件必须被
    <Suspense>
    边界包裹
这是Next.js的强制要求,并非可选!

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.
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

TypeScript:切勿使用
any
类型

typescript
// ❌ 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对比

FeatureServer ComponentClient 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 forInitial load, SEODynamic filters, real-time updates
特性服务端组件客户端组件
访问方式
searchParams
属性
useSearchParams()
钩子
是否需要'use client'❌ 不需要✅ 需要
是否需要Suspense❌ 不需要✅ 需要
是否支持异步✅ 支持❌ 不支持
是否可更新参数❌ 不支持(使用Link/redirect)✅ 支持(使用router.push)
适用场景初始加载、SEO优化动态筛选、实时更新

Quick Checklist

快速检查清单

When using useSearchParams:
  • Add
    'use client'
    directive at top of file
  • Import
    Suspense
    from 'react'
  • Import
    useSearchParams
    from 'next/navigation'
  • Wrap component using
    useSearchParams
    in
    <Suspense>
  • Provide a fallback to Suspense
  • Call
    useSearchParams()
    inside wrapped component
  • Use
    .get()
    ,
    .has()
    , or
    .getAll()
    to read params
使用useSearchParams时:
  • 在文件顶部添加
    'use client'
    指令
  • 从'react'中导入
    Suspense
  • 从'next/navigation'中导入
    useSearchParams
  • 将使用
    useSearchParams
    的组件用
    <Suspense>
    包裹
  • 为Suspense提供fallback内容
  • 在被包裹的组件内部调用
    useSearchParams()
  • 使用
    .get()
    .has()
    .getAll()
    读取参数

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.
结合Suspense使用useSearchParams:
  • ✅ 必须包含
    'use client'
    指令
  • ✅ 必须被
    <Suspense>
    包裹
  • ✅ 用于客户端侧的URL参数读取
  • ✅ 可与
    useRouter()
    结合更新参数
  • ✅ 最适用于筛选器、搜索、分页功能
  • ❌ 不可在服务端组件中使用(改用
    searchParams
    属性)
这是Next.js App Router中处理客户端URL参数的推荐模式。