mcpField
N
nextjs-use-search-params-suspense

nextjs-use-search-params-suspense

github.com/wsimmonds开发109312 次安装53 周前更新
代码仓库

关于 nextjs-use-search-params-suspense

面向 Next.js App Router 开发者的 useSearchParams 模式指南,说明如何在客户端组件中读取 URL 查询参数。重点是使用 Suspense 包裹组件,并区分客户端 Hook 与服务器端 searchParams 属性。

主要功能

  1. 01客户端组件中读取查询参数
  2. 02使用 Suspense 边界和回退内容
  3. 03通过 get、has、getAll 访问参数
  4. 04配合 useRouter 更新 URL 参数
  5. 05说明服务器组件的 searchParams 用法

适用场景

  1. 01构建基于 URL 的搜索页面
  2. 02读取和更新商品筛选条件
  3. 03实现分页查询
  4. 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 hook 有两项要求:

  1. 组件必须包含 'use client' 指令
  2. 组件必须包裹在 <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>
  );
}
内联模式(单文件)

有时你可能希望将所有内容放在一个文件中:

// 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 propuseSearchParams() hook
是否需要 'use client'❌ 否✅ 是
是否需要 Suspense❌ 否✅ 是
是否可以异步✅ 是❌ 否
是否可以更新参数❌ 否(使用 Link/redirect)✅ 是(使用 router.push)
适用场景初始加载、SEO动态筛选、实时更新

快速检查清单

使用 useSearchParams 时:

  • 在文件顶部添加 'use client' 指令
  • 从 'react' 导入 Suspense
  • 从 'next/navigation' 导入 useSearchParams
  • 将使用 useSearchParams 的组件包裹在 <Suspense> 中
  • 为 Suspense 提供 fallback
  • 在被包裹的组件内部调用 useSearchParams()
  • 使用 .get()、.has() 或 .getAll() 读取参数

总结

在 Suspense 模式下使用 useSearchParams:

  • ✅ 需要 'use client' 指令
  • ✅ 需要 <Suspense> 包装
  • ✅ 用于在客户端读取 URL 参数
  • ✅ 与 useRouter() 结合使用以更新参数
  • ✅ 最适用于筛选、搜索、分页
  • ❌ 不适用于服务器组件(请改用 searchParams prop)

这是在 Next.js App Router 中处理客户端 URL 参数的推荐模式。

评论

支持 Markdown