Smithery Logo
MCPsSkillsDocsPricing
Login
Smithery Logo

Accelerating the Agent Economy

Resources

DocumentationPrivacy PolicySystem Status

Company

PricingAboutBlog

Connect

© 2026 Smithery. All rights reserved.

    wsimmonds

    nextjs-use-search-params-suspense

    wsimmonds/nextjs-use-search-params-suspense
    Coding
    62
    1 installs

    About

    SKILL.md

    Install

    Install via Skills CLI

    or add to your agent
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    ├─
    ├─
    └─

    About

    Pattern for using useSearchParams hook with Suspense boundary in Next.js...

    SKILL.md

    Next.js: useSearchParams with Suspense Pattern

    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!

    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 (
        <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:

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

    // ❌ 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 (
        <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>
      );
    }
    

    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 (
        <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>
      );
    }
    

    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 (
        <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>
      );
    }
    

    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 <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>) => {
        // 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>
      );
    }
    

    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 (
        <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>
      );
    }
    

    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 = 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>
      );
    }
    

    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 <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>;
    }
    

    ❌ 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 <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>
      );
    }
    

    ❌ 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 <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>;
    }
    

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

    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.

    Recommended Servers
    Vercel Grep
    Vercel Grep
    Brave Search
    Brave Search
    Jina AI
    Jina AI
    Repository
    wsimmonds/claude-nextjs-skills
    Files