Building dynamic navigation requires knowing where users are in your application. The usePathname hook tells you the current route path, while useSearchParams gives you access to URL query parameters. Together, these hooks enable you to highlight active links, build dynamic breadcrumbs, read filters from URLs, and create navigation UI that responds to the current route. Let's master reading and working with URL state!
usePathname Hook
The usePathname hook returns the current pathname:
Basic Usage
'use client';
import { usePathname } from 'next/navigation';
export function CurrentPath() {
const pathname = usePathname();
return (
<div className="p-4 bg-gray-100 rounded">
<p className="text-sm text-gray-600">Current path:</p>
<p className="font-mono font-bold">{pathname}</p>
</div>
);
}
// URL: https://example.com/blog/my-post?sort=recent
// pathname: "/blog/my-post"
// URL: https://example.com/products
// pathname: "/products"
// URL: https://example.com/
// pathname: "/"
// ✅ Returns just the pathname
// ✅ No query parameters
// ✅ No domain or protocol
// ✅ No hash fragmentConditional Rendering Based on Path
'use client';
import { usePathname } from 'next/navigation';
export function ConditionalNav() {
const pathname = usePathname();
// Show different nav for different sections
const isHome = pathname === '/';
const isBlog = pathname.startsWith('/blog');
const isAdmin = pathname.startsWith('/admin');
if (isAdmin) {
return <AdminNav />;
}
if (isBlog) {
return <BlogNav />;
}
return <MainNav />;
}
// ✅ Different navigation for different sections
// ✅ Check exact path with ===
// ✅ Check path prefix with startsWith()Dynamic Breadcrumbs
'use client';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
export function Breadcrumbs() {
const pathname = usePathname();
// Split path into segments
const segments = pathname.split('/').filter(Boolean);
// Build breadcrumb items
const breadcrumbs = segments.map((segment, index) => {
const href = '/' + segments.slice(0, index + 1).join('/');
const label = segment
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
return { href, label };
});
return (
<nav className="flex items-center gap-2 text-sm mb-6">
<Link
href="/"
className="text-gray-600 hover:text-blue-600 transition"
>
Home
</Link>
{breadcrumbs.map((crumb, index) => {
const isLast = index === breadcrumbs.length - 1;
return (
<div key={crumb.href} className="flex items-center gap-2">
<span className="text-gray-400">/</span>
{isLast ? (
<span className="text-gray-900 font-semibold">
{crumb.label}
</span>
) : (
<Link
href={crumb.href}
className="text-gray-600 hover:text-blue-600 transition"
>
{crumb.label}
</Link>
)}
</div>
);
})}
</nav>
);
}
// URL: /blog/tech/nextjs-tutorial
// Breadcrumbs: Home / Blog / Tech / Nextjs Tutorial
// ^ ^ ^ ^
// / /blog /blog/tech /blog/tech/nextjs-tutorial
// ✅ Automatically generates breadcrumbs from path
// ✅ Each segment is clickable (except last)
// ✅ Formats segment names nicelyusePathname vs Server Component params
Server Components receive the pathname via props. Only use usePathname in Client Components:
// Server Component - use props
async function ServerPage({ params }: { params: { slug: string } }) {
const slug = params.slug; // From route parameters
return <div>{slug}</div>;
}
// Client Component - use usePathname
'use client';
function ClientComponent() {
const pathname = usePathname(); // From hook
return <div>{pathname}</div>;
}useSearchParams Hook
The useSearchParams hook returns a read-only URLSearchParams object:
Basic Usage
'use client';
import { useSearchParams } from 'next/navigation';
export function SearchParamsDisplay() {
const searchParams = useSearchParams();
// Get specific parameter
const query = searchParams.get('q');
const category = searchParams.get('category');
const page = searchParams.get('page');
return (
<div className="p-4 bg-gray-100 rounded space-y-2">
<p><strong>Query:</strong> {query || 'None'}</p>
<p><strong>Category:</strong> {category || 'None'}</p>
<p><strong>Page:</strong> {page || '1'}</p>
</div>
);
}
// URL: /search?q=nextjs&category=tutorials&page=2
// query: "nextjs"
// category: "tutorials"
// page: "2"
// URL: /search
// query: null
// category: null
// page: null
// ✅ Get individual parameters with .get()
// ✅ Returns null if parameter doesn't exist
// ✅ All values are stringsCommon SearchParams Methods
'use client';
import { useSearchParams } from 'next/navigation';
export function SearchParamsExample() {
const searchParams = useSearchParams();
// Get single value
const sort = searchParams.get('sort');
// ?sort=recent → "recent"
// Get all values for parameter (for arrays)
const tags = searchParams.getAll('tag');
// ?tag=react&tag=nextjs → ["react", "nextjs"]
// Check if parameter exists
const hasFilter = searchParams.has('filter');
// ?filter=active → true
// ?other=value → false
// Get all parameters as object
const allParams = Object.fromEntries(searchParams.entries());
// ?sort=recent&page=2 → { sort: "recent", page: "2" }
// Convert to string
const queryString = searchParams.toString();
// ?sort=recent&page=2 → "sort=recent&page=2"
return (
<div>
<p>Sort: {sort}</p>
<p>Tags: {tags.join(', ')}</p>
<p>Has filter: {hasFilter ? 'Yes' : 'No'}</p>
<p>All params: {JSON.stringify(allParams)}</p>
<p>Query string: {queryString}</p>
</div>
);
}
// ✅ .get(key) - get single value
// ✅ .getAll(key) - get array of values
// ✅ .has(key) - check if exists
// ✅ .entries() - get all as entries
// ✅ .toString() - convert to query stringSuspense Boundary Requirement
import { Suspense } from 'react';
// Component that uses useSearchParams
function SearchResults() {
'use client';
const searchParams = useSearchParams();
const query = searchParams.get('q');
return <div>Results for: {query}</div>;
}
// Page must wrap in Suspense
export default function SearchPage() {
return (
<div>
<h1>Search</h1>
{/* Wrap useSearchParams components in Suspense */}
<Suspense fallback={<div>Loading...</div>}>
<SearchResults />
</Suspense>
</div>
);
}
// ✅ Prevents entire page from client-side rendering
// ✅ Better performance
// ✅ Allows streaming
// ⚠️ Without Suspense:
// - Component de-opts to client-side rendering
// - Hurts performance
// - No streaming⚠️ SearchParams are Read-Only
useSearchParams returns a read-only URLSearchParams. To update parameters, use useRouter:
'use client';
import { useSearchParams, useRouter } from 'next/navigation';
function FilterButton() {
const searchParams = useSearchParams();
const router = useRouter();
const updateFilter = (filter: string) => {
// Create new URLSearchParams
const params = new URLSearchParams(searchParams.toString());
params.set('filter', filter);
// Navigate with new params
router.push(`?${params.toString()}`);
};
return (
<button onClick={() => updateFilter('active')}>
Filter Active
</button>
);
}Practical Examples
Example 1: Active Navigation Links
'use client';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
interface ActiveLinkProps {
href: string;
children: React.ReactNode;
}
export function ActiveLink({ href, children }: ActiveLinkProps) {
const pathname = usePathname();
const isActive = pathname === href || pathname.startsWith(href + '/');
return (
<Link
href={href}
className={`px-4 py-2 rounded-lg transition ${
isActive
? 'bg-blue-600 text-white font-semibold'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
{children}
</Link>
);
}
// Usage in navigation
export function Navigation() {
return (
<nav className="flex gap-2">
<ActiveLink href="/">Home</ActiveLink>
<ActiveLink href="/blog">Blog</ActiveLink>
<ActiveLink href="/about">About</ActiveLink>
<ActiveLink href="/contact">Contact</ActiveLink>
</nav>
);
}
// ✅ Active link is highlighted
// ✅ Works with nested routes (/blog/post-1 highlights /blog)
// ✅ Visual feedback for current pageExample 2: Search Filters with URL State
'use client';
import { useSearchParams, useRouter } from 'next/navigation';
import { useCallback } from 'react';
export function SearchFilters() {
const searchParams = useSearchParams();
const router = useRouter();
// Get current values
const currentCategory = searchParams.get('category') || 'all';
const currentSort = searchParams.get('sort') || 'recent';
// Update filter function
const updateFilter = useCallback((key: string, value: string) => {
const params = new URLSearchParams(searchParams.toString());
if (value === 'all') {
params.delete(key);
} else {
params.set(key, value);
}
router.push(`?${params.toString()}`, { scroll: false });
}, [searchParams, router]);
return (
<div className="space-y-4 p-4 bg-white rounded-lg shadow">
{/* Category Filter */}
<div>
<label className="block text-sm font-medium mb-2">
Category
</label>
<select
value={currentCategory}
onChange={(e) => updateFilter('category', e.target.value)}
className="w-full px-3 py-2 border rounded-lg"
>
<option value="all">All Categories</option>
<option value="tech">Tech</option>
<option value="design">Design</option>
<option value="business">Business</option>
</select>
</div>
{/* Sort Filter */}
<div>
<label className="block text-sm font-medium mb-2">
Sort By
</label>
<select
value={currentSort}
onChange={(e) => updateFilter('sort', e.target.value)}
className="w-full px-3 py-2 border rounded-lg"
>
<option value="recent">Most Recent</option>
<option value="popular">Most Popular</option>
<option value="title">Title (A-Z)</option>
</select>
</div>
{/* Clear Filters */}
{(currentCategory !== 'all' || currentSort !== 'recent') && (
<button
onClick={() => router.push(window.location.pathname)}
className="w-full px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300"
>
Clear Filters
</button>
)}
</div>
);
}
// URL updates: /blog?category=tech&sort=popular
// ✅ Filters stored in URL
// ✅ Shareable URLs
// ✅ Browser back/forward works
// ✅ Preserves scroll position (scroll: false)Example 3: Pagination with Search Params
'use client';
import { useSearchParams, useRouter } from 'next/navigation';
interface PaginationProps {
totalPages: number;
}
export function Pagination({ totalPages }: PaginationProps) {
const searchParams = useSearchParams();
const router = useRouter();
const currentPage = Number(searchParams.get('page')) || 1;
const goToPage = (page: number) => {
const params = new URLSearchParams(searchParams.toString());
params.set('page', page.toString());
router.push(`?${params.toString()}`);
};
const pages = Array.from({ length: totalPages }, (_, i) => i + 1);
return (
<div className="flex items-center justify-center gap-2">
{/* Previous Button */}
<button
onClick={() => goToPage(currentPage - 1)}
disabled={currentPage === 1}
className="px-4 py-2 border rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-100"
>
← Previous
</button>
{/* Page Numbers */}
{pages.map(page => {
// Show first, last, current, and adjacent pages
if (
page === 1 ||
page === totalPages ||
(page >= currentPage - 1 && page <= currentPage + 1)
) {
return (
<button
key={page}
onClick={() => goToPage(page)}
className={`px-4 py-2 border rounded-lg ${
page === currentPage
? 'bg-blue-600 text-white'
: 'hover:bg-gray-100'
}`}
>
{page}
</button>
);
}
// Show ellipsis
if (page === currentPage - 2 || page === currentPage + 2) {
return <span key={page} className="px-2">...</span>;
}
return null;
})}
{/* Next Button */}
<button
onClick={() => goToPage(currentPage + 1)}
disabled={currentPage === totalPages}
className="px-4 py-2 border rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-100"
>
Next →
</button>
</div>
);
}
// URL: /blog?page=3
// ✅ Page stored in URL
// ✅ Shareable links to specific pages
// ✅ Browser back/forward works
// ✅ Smart ellipsis for many pagesExample 4: Multi-Select Filters
'use client';
import { useSearchParams, useRouter } from 'next/navigation';
const AVAILABLE_TAGS = ['react', 'nextjs', 'typescript', 'tailwind', 'node'];
export function TagFilter() {
const searchParams = useSearchParams();
const router = useRouter();
// Get current tags from URL
const selectedTags = searchParams.getAll('tag');
const toggleTag = (tag: string) => {
const params = new URLSearchParams(searchParams.toString());
if (selectedTags.includes(tag)) {
// Remove tag - need to rebuild params without this tag
params.delete('tag');
selectedTags
.filter(t => t !== tag)
.forEach(t => params.append('tag', t));
} else {
// Add tag
params.append('tag', tag);
}
router.push(`?${params.toString()}`, { scroll: false });
};
return (
<div className="space-y-3">
<h3 className="font-semibold">Filter by Tags</h3>
<div className="flex flex-wrap gap-2">
{AVAILABLE_TAGS.map(tag => {
const isSelected = selectedTags.includes(tag);
return (
<button
key={tag}
onClick={() => toggleTag(tag)}
className={`px-4 py-2 rounded-lg border transition ${
isSelected
? 'bg-blue-600 text-white border-blue-600'
: 'bg-white text-gray-700 border-gray-300 hover:border-blue-600'
}`}
>
{tag}
{isSelected && ' ✓'}
</button>
);
})}
</div>
{selectedTags.length > 0 && (
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600">
{selectedTags.length} tag{selectedTags.length !== 1 ? 's' : ''} selected
</span>
<button
onClick={() => router.push(window.location.pathname)}
className="text-sm text-blue-600 hover:underline"
>
Clear all
</button>
</div>
)}
</div>
);
}
// URL: /blog?tag=react&tag=nextjs&tag=typescript
// ✅ Multiple values for same parameter
// ✅ Toggle on/off
// ✅ Visual feedback
// ✅ Clear all optionPathname and SearchParams Examples
Project structure with usePathname and useSearchParams patterns
Select a file or folder to see details
Combining Pathname and SearchParams
'use client';
import { usePathname, useSearchParams } from 'next/navigation';
import Link from 'next/link';
export function SmartBreadcrumbs() {
const pathname = usePathname();
const searchParams = useSearchParams();
const segments = pathname.split('/').filter(Boolean);
// Build breadcrumbs with current filters
const currentParams = searchParams.toString();
const paramsString = currentParams ? `?${currentParams}` : '';
const breadcrumbs = segments.map((segment, index) => {
const href = '/' + segments.slice(0, index + 1).join('/');
const label = segment
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
return { href, label };
});
return (
<nav className="flex items-center gap-2 text-sm mb-6">
<Link
href={`/${paramsString}`}
className="text-gray-600 hover:text-blue-600 transition"
>
Home
</Link>
{breadcrumbs.map((crumb, index) => {
const isLast = index === breadcrumbs.length - 1;
const hrefWithParams = `${crumb.href}${paramsString}`;
return (
<div key={crumb.href} className="flex items-center gap-2">
<span className="text-gray-400">/</span>
{isLast ? (
<span className="text-gray-900 font-semibold">
{crumb.label}
</span>
) : (
<Link
href={hrefWithParams}
className="text-gray-600 hover:text-blue-600 transition"
>
{crumb.label}
</Link>
)}
</div>
);
})}
{/* Show active filters */}
{currentParams && (
<span className="ml-2 px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded">
Filtered
</span>
)}
</nav>
);
}
// URL: /blog/tech?category=tutorials&sort=recent
// Breadcrumbs: Home / Blog / Tech (Filtered)
// ✅ Combines pathname and searchParams
// ✅ Preserves filters in breadcrumb links
// ✅ Shows filter indicatorBest Practices
1. Wrap useSearchParams in Suspense
// ✅ GOOD: Wrapped in Suspense
import { Suspense } from 'react';
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<SearchComponent />
</Suspense>
);
}
// ❌ BAD: No Suspense (de-opts to client-side rendering)
export default function Page() {
return <SearchComponent />;
}2. Use scroll: false for Filter Updates
// ✅ GOOD: Don't scroll to top on filter change
router.push(`?${params.toString()}`, { scroll: false });
// ❌ BAD: Scrolls to top on every filter
router.push(`?${params.toString()}`);3. Provide Default Values
// ✅ GOOD: Default values for missing params
const page = Number(searchParams.get('page')) || 1;
const category = searchParams.get('category') || 'all';
const sort = searchParams.get('sort') || 'recent';
// ❌ BAD: No defaults (null values)
const page = searchParams.get('page');
const category = searchParams.get('category');4. Use useCallback for Update Functions
// ✅ GOOD: Memoize update function
const updateFilter = useCallback((key: string, value: string) => {
const params = new URLSearchParams(searchParams.toString());
params.set(key, value);
router.push(`?${params.toString()}`);
}, [searchParams, router]);
// Prevents unnecessary re-renders5. Clean Up Empty Parameters
// ✅ GOOD: Remove empty parameters
const updateFilter = (key: string, value: string) => {
const params = new URLSearchParams(searchParams.toString());
if (!value || value === 'all') {
params.delete(key); // Remove empty params
} else {
params.set(key, value);
}
router.push(`?${params.toString()}`);
};
// URL: /blog?category=tech (clean)
// Not: /blog?category=tech&sort= (dirty)Key Takeaways
- usePathname() - returns current pathname string
- useSearchParams() - returns URLSearchParams object
- Client Components only - require 'use client'
- Wrap in Suspense - useSearchParams needs Suspense
- Read-only - use useRouter to update params
- Active links - compare pathname for highlighting
- URL state - store filters, page, sort in URL
- Shareable - URLs with params are shareable
What's Next?
You've mastered reading pathname and search parameters! Next, we'll explore Active Links and Navigation States—advanced techniques for styling active links, managing navigation states, showing loading indicators during navigation, and creating sophisticated navigation UI.
You'll learn to build professional navigation components with proper visual feedback, loading states, and user-friendly interactions.
🔗 URL as State
Using URL parameters for filters, pagination, and sort order is a best practice. It makes URLs shareable, enables browser back/forward, and improves SEO. Always consider storing UI state in the URL when possible!