Fix: nuqs Not Working — URL State Not Syncing, Type Errors, or Server Component Issues
Quick Answer
How to fix nuqs URL search params state management — useQueryState and useQueryStates setup, parsers, server-side access, shallow routing, history mode, and Next.js App Router integration.
The Problem
useQueryState updates state but the URL doesn’t change:
import { useQueryState } from 'nuqs';
function SearchPage() {
const [query, setQuery] = useQueryState('q');
// setQuery('hello') updates the component but URL stays the same
}Or the URL changes but the component doesn’t re-render:
const [page, setPage] = useQueryState('page', parseAsInteger);
// URL shows ?page=2 but component still renders page 1Or using nuqs in a Server Component throws:
Error: useQueryState is a client hook and cannot be used in Server ComponentsOr default values are null instead of a fallback:
const [sort, setSort] = useQueryState('sort');
console.log(sort); // null — not 'newest' as expectedWhy This Happens
nuqs manages React state that’s synchronized with URL search parameters. It runs client-side and requires specific setup:
- nuqs needs a provider in Next.js App Router —
NuqsAdaptermust wrap your app. Without it, state updates work in memory but don’t sync to the URL. - Parsers determine the type —
useQueryState('page')returns astring | nullby default. For numbers, useparseAsInteger. For booleans, useparseAsBoolean. Without a parser, the value is always a string. - Server Components can’t use hooks —
useQueryStateis a React hook. For Server Components, readsearchParamsdirectly from the page props. nuqs providescreateSearchParamsCachefor type-safe server-side access. - Default values require
.withDefault()— without it, the initial value isnullwhen the URL parameter is absent. Chain.withDefault('value')to set a fallback.
Fix 1: Basic Setup with Next.js App Router
npm install nuqs// app/layout.tsx — add the adapter
import { NuqsAdapter } from 'nuqs/adapters/next/app';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<NuqsAdapter>{children}</NuqsAdapter>
</body>
</html>
);
}
// For Pages Router:
// import { NuqsAdapter } from 'nuqs/adapters/next/pages';
// Wrap in _app.tsx// components/SearchFilter.tsx — client component
'use client';
import { useQueryState, parseAsInteger, parseAsStringEnum } from 'nuqs';
const sortOptions = ['newest', 'oldest', 'popular'] as const;
function SearchFilter() {
// String parameter — ?q=hello
const [query, setQuery] = useQueryState('q', {
defaultValue: '',
shallow: false, // Trigger server-side data refetch
});
// Integer parameter — ?page=2
const [page, setPage] = useQueryState(
'page',
parseAsInteger.withDefault(1)
);
// Enum parameter — ?sort=newest
const [sort, setSort] = useQueryState(
'sort',
parseAsStringEnum(sortOptions).withDefault('newest')
);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value || null)}
placeholder="Search..."
/>
<select value={sort} onChange={(e) => setSort(e.target.value as typeof sort)}>
{sortOptions.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
<div>
<button
onClick={() => setPage(p => Math.max(1, (p ?? 1) - 1))}
disabled={page <= 1}
>
Previous
</button>
<span>Page {page}</span>
<button onClick={() => setPage(p => (p ?? 1) + 1)}>
Next
</button>
</div>
{/* Reset all — set to null removes from URL */}
<button onClick={() => {
setQuery(null);
setPage(null);
setSort(null);
}}>
Clear Filters
</button>
</div>
);
}Fix 2: Multiple Parameters with useQueryStates
'use client';
import { useQueryStates, parseAsInteger, parseAsBoolean, parseAsStringEnum } from 'nuqs';
function ProductFilters() {
const [filters, setFilters] = useQueryStates({
q: { defaultValue: '' },
page: parseAsInteger.withDefault(1),
perPage: parseAsInteger.withDefault(20),
sort: parseAsStringEnum(['price', 'name', 'rating']).withDefault('name'),
inStock: parseAsBoolean.withDefault(false),
minPrice: parseAsInteger,
maxPrice: parseAsInteger,
}, {
shallow: false, // Refetch data on change
});
// filters.q, filters.page, filters.sort, etc. — all typed
// URL: ?q=shoes&page=2&sort=price&inStock=true&minPrice=20
return (
<div>
<input
value={filters.q}
onChange={(e) => setFilters({ q: e.target.value || null, page: 1 })}
/>
<label>
<input
type="checkbox"
checked={filters.inStock}
onChange={(e) => setFilters({ inStock: e.target.checked, page: 1 })}
/>
In Stock Only
</label>
<select
value={filters.sort}
onChange={(e) => setFilters({ sort: e.target.value as any })}
>
<option value="name">Name</option>
<option value="price">Price</option>
<option value="rating">Rating</option>
</select>
{/* Reset all filters */}
<button onClick={() => setFilters(null)}>Clear All</button>
</div>
);
}Fix 3: Server-Side Access
// lib/searchParams.ts — define parsers once, use on server and client
import { createSearchParamsCache, parseAsInteger, parseAsString, parseAsStringEnum } from 'nuqs/server';
export const searchParamsCache = createSearchParamsCache({
q: parseAsString.withDefault(''),
page: parseAsInteger.withDefault(1),
sort: parseAsStringEnum(['newest', 'oldest', 'popular']).withDefault('newest'),
});// app/posts/page.tsx — Server Component
import { searchParamsCache } from '@/lib/searchParams';
import { SearchFilter } from '@/components/SearchFilter';
export default async function PostsPage({
searchParams,
}: {
searchParams: Promise<Record<string, string | string[]>>;
}) {
// Parse and validate search params on the server
const { q, page, sort } = searchParamsCache.parse(await searchParams);
// Fetch data with validated params
const posts = await db.query.posts.findMany({
where: q ? like(posts.title, `%${q}%`) : undefined,
orderBy: sort === 'newest' ? desc(posts.createdAt) : asc(posts.createdAt),
limit: 20,
offset: (page - 1) * 20,
});
return (
<div>
<SearchFilter /> {/* Client component uses useQueryState */}
<PostList posts={posts} />
</div>
);
}Fix 4: Custom Parsers
import { createParser } from 'nuqs';
// Date parser — ?date=2024-03-15
const parseAsDate = createParser({
parse: (value) => {
const date = new Date(value);
return isNaN(date.getTime()) ? null : date;
},
serialize: (date) => date.toISOString().split('T')[0],
});
// Array parser — ?tags=react,typescript,nextjs
const parseAsCommaArray = createParser({
parse: (value) => value.split(',').filter(Boolean),
serialize: (arr) => arr.join(','),
});
// JSON parser — ?config={"theme":"dark","lang":"en"}
const parseAsJson = <T>() => createParser<T>({
parse: (value) => {
try {
return JSON.parse(value);
} catch {
return null;
}
},
serialize: (obj) => JSON.stringify(obj),
});
// Usage
function FilteredPage() {
const [date, setDate] = useQueryState('date', parseAsDate);
const [tags, setTags] = useQueryState('tags', parseAsCommaArray.withDefault([]));
return (
<div>
<input
type="date"
value={date?.toISOString().split('T')[0] ?? ''}
onChange={(e) => setDate(e.target.value ? new Date(e.target.value) : null)}
/>
{tags.map(tag => (
<span key={tag}>
{tag}
<button onClick={() => setTags(tags.filter(t => t !== tag))}>×</button>
</span>
))}
</div>
);
}Fix 5: History Mode and Shallow Routing
'use client';
import { useQueryState, parseAsInteger } from 'nuqs';
function HistoryOptions() {
// Default: replace history entry (back button skips intermediate states)
const [tab, setTab] = useQueryState('tab', {
history: 'replace', // Default — doesn't add history entries
defaultValue: 'overview',
});
// Push: each change adds a history entry (back button goes to previous state)
const [page, setPage] = useQueryState('page', {
...parseAsInteger.withDefault(1),
history: 'push', // Back button goes to previous page
});
// Shallow: don't trigger Next.js server-side data fetching
const [view, setView] = useQueryState('view', {
defaultValue: 'grid',
shallow: true, // Client-only state, no server refetch
});
// Non-shallow: trigger server-side refetch (default in App Router)
const [query, setQuery] = useQueryState('q', {
defaultValue: '',
shallow: false, // Server component re-renders with new searchParams
});
// Throttle URL updates for rapid changes (e.g., range sliders)
const [price, setPrice] = useQueryState('price', {
...parseAsInteger.withDefault(0),
throttleMs: 300, // Update URL at most every 300ms
});
return (
<div>
<input
type="range"
value={price}
min={0}
max={1000}
onChange={(e) => setPrice(Number(e.target.value))}
/>
<span>${price}</span>
</div>
);
}Fix 6: Common Patterns
// Debounced search with nuqs
'use client';
import { useQueryState } from 'nuqs';
import { useState, useEffect } from 'react';
function DebouncedSearch() {
const [query, setQuery] = useQueryState('q', {
defaultValue: '',
shallow: false,
throttleMs: 500, // Built-in throttle
});
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value || null)}
placeholder="Search..."
/>
);
}
// Shareable filter URLs
function ShareFilters() {
const [filters] = useQueryStates({
q: { defaultValue: '' },
category: { defaultValue: '' },
sort: parseAsStringEnum(['price', 'rating']).withDefault('rating'),
});
function copyShareLink() {
// URL is already updated — just copy it
navigator.clipboard.writeText(window.location.href);
toast.success('Link copied!');
}
return <button onClick={copyShareLink}>Share Filters</button>;
}Still Not Working?
State updates but URL doesn’t change — NuqsAdapter is missing. Wrap your app in <NuqsAdapter> in the root layout. Without it, nuqs can’t interact with the router. For Next.js App Router, use nuqs/adapters/next/app. For Pages Router, use nuqs/adapters/next/pages.
URL changes but component doesn’t re-render — check the shallow option. With shallow: true (or default in some cases), Next.js doesn’t re-run the page’s server component. Set shallow: false to trigger server-side data refetching. For client-only state, ensure the component reads from useQueryState, not from searchParams.
Value is null instead of a default — chain .withDefault() on the parser: parseAsInteger.withDefault(1). Without it, absent URL params resolve to null. Setting to null removes the parameter from the URL.
Type mismatch — expected number, got string — use the correct parser. useQueryState('page') returns string | null. For numbers, use parseAsInteger or parseAsFloat. For booleans, use parseAsBoolean. Parsers handle serialization/deserialization between the URL string and the typed value.
For related state management issues, see Fix: Zustand Not Working and Fix: Next.js App Router Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: React Hydration Error — Text Content Does Not Match
How to fix React hydration errors — server/client HTML mismatches, useEffect for client-only code, suppressHydrationWarning, dynamic content, and Next.js specific hydration issues.
Fix: React Server Components Error — useState, Event Handlers, and Client Boundary Issues
How to fix React Server Components errors — useState and hooks in server components, missing 'use client' directive, async component patterns, serialization errors, and client/server boundary mistakes.
Fix: Clerk Not Working — Auth Not Loading, Middleware Blocking, or User Data Missing
How to fix Clerk authentication issues — ClerkProvider setup, middleware configuration, useUser and useAuth hooks, server-side auth, webhook handling, and organization features.
Fix: cmdk Not Working — Command Palette Not Opening, Items Not Filtering, or Keyboard Navigation Broken
How to fix cmdk command palette issues — Dialog setup, custom filtering, groups and separators, keyboard shortcuts, async search, nested pages, and integration with shadcn/ui and Tailwind.