Server-Side Data Fetching + URL Search Params with nuqs in Next.js

Mohamed Almadih

When building dashboards in Next.js, you often need filters such as search bars, dropdowns, and pagination controls — all while keeping server-side rendering (SSR) for SEO and performance.
One great tool for managing URL search parameters is nuqs
. It makes it easy to read, write, and sync query params in Next.js.
In this post, we'll focus on:
- Using
nuqs
for query params in a server-side data fetching setup - Avoiding slow, laggy typing in search bars
- Keeping concerns separated for maintainability
The Problem
You have a Next.js app with:
- A dashboard page
- A server-side fetched transactions table
- A search bar that updates the URL's query string
- Data fetched server-side based on those query params
The setup looks like this:
Client-side Filter Component
1// app/_components/search-bar.tsx2"use client";34import { useQueryState, parseAsString } from "nuqs";56export default function SearchBar() {7const [search, setSearch] = useQueryState(8"search",9parseAsString.withDefault("").withOptions({ shallow: false })10);1112const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {13setSearch(event.target.value);14};1516return (17<input18type="text"19value={search}20onChange={handleSearch}21placeholder="Search transactions..."22className="border p-2 rounded"23/>24);25}
Server-side Page
1// app/dashboard/page.tsx2import { createSearchParamsCache, parseAsString } from "nuqs/server";3import { auth } from "@/lib/auth";4import { fetchTransactions } from "@/lib/transactions";56type SearchParams = {7search?: string;8}910const searchParamsCache = createSearchParamsCache({11search: parseAsString.withDefault(""),12// other params...13});1415export default async function DashboardPage(props: { searchParams: Promise<SearchParams> }) {16const session = await auth.getSession();17const searchParams = await searchParamsCache.parse(props.searchParams);1819const filters = {20search: searchParams.search,21// other filters...22};2324const transactions = await fetchTransactions(filters, session.user.id);2526return (27<div>28{/* render filters and table */}29</div>30);31}
The Issue
When typing quickly in the search bar, it feels slow — characters sometimes don't show immediately.
Why? Because every keystroke updates the URL → triggers a React state change → transitions → potentially heavy re-renders.
Even though the data is fetched server-side, the client input updates are tied too closely to URL updates.
The Solution: Debounce the URL Updates
Instead of updating the query param on every keystroke, we debounce the updates.
This means:
- The input value updates immediately for the user
- The URL (and thus server refetch) only updates after the user stops typing for a short delay
Updated Search Component with Debounce
w'll use use-debounce
package to debounce the updates.
install it with npm i use-debounce
or yarn add use-debounce
1"use client";23import { useQueryState, parseAsString } from "nuqs";4import { useEffect } from "react";5import { useDebounce } from "use-debounce";67export default function SearchBar() {8const [search, setSearch] = useQueryState(9"search",10parseAsString.withDefault("").withOptions({ shallow: false })11);1213const [inputValue, setInputValue] = useState(search);14const [debouncedValue] = useDebounce(inputValue, 250);1516useEffect(() => {17startSearchTransition(() => {18setSearch(debouncedValue);19});20console.log(debouncedValue);21}, [debouncedValue, setSearch]);2223return (24<input25type="text"26value={inputValue}27onChange={(e) => setInputValue(e.target.value)}28placeholder="Search transactions..."29className="border p-2 rounded"30/>31);32}
Why This Works
- Immediate feedback: The
inputValue
state updates instantly, so the user sees their typing without delay. - Reduced URL churn: The
setSearch
call only happens after 500ms of inactivity. - Efficient SSR: The server still receives query params and fetches filtered data on page load or param changes.
- Separation of concerns: Client-side handles UI responsiveness; server-side handles the heavy lifting.
General Best Practices
- Keep filters/search params in the URL for shareability and SSR.
- Use
nuqs
for safe, typed query param parsing. - Debounce inputs that trigger server refetches.
- Separate components:
- Search/filter dropdowns → client
- Data fetching & rendering → server
- Pass props from server to client components for initial state, but always let the server be the source of truth.
Final Thoughts
By using nuqs
with a debounce strategy, you can have instant, smooth search input while keeping your data fetching server-driven and SEO-friendly.
This approach is clean, maintainable, and keeps performance in check — especially for dashboards where filtering is frequent.