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

Mohamed Almadih

Mohamed Almadih

8/15/2025
4 min read
Server-Side Data Fetching + URL Search Params with nuqs in Next.js

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.tsx
2
"use client";
3
4
import { useQueryState, parseAsString } from "nuqs";
5
6
export default function SearchBar() {
7
const [search, setSearch] = useQueryState(
8
"search",
9
parseAsString.withDefault("").withOptions({ shallow: false })
10
);
11
12
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
13
setSearch(event.target.value);
14
};
15
16
return (
17
<input
18
type="text"
19
value={search}
20
onChange={handleSearch}
21
placeholder="Search transactions..."
22
className="border p-2 rounded"
23
/>
24
);
25
}

Server-side Page

1
// app/dashboard/page.tsx
2
import { createSearchParamsCache, parseAsString } from "nuqs/server";
3
import { auth } from "@/lib/auth";
4
import { fetchTransactions } from "@/lib/transactions";
5
6
type SearchParams = {
7
search?: string;
8
}
9
10
const searchParamsCache = createSearchParamsCache({
11
search: parseAsString.withDefault(""),
12
// other params...
13
});
14
15
export default async function DashboardPage(props: { searchParams: Promise<SearchParams> }) {
16
const session = await auth.getSession();
17
const searchParams = await searchParamsCache.parse(props.searchParams);
18
19
const filters = {
20
search: searchParams.search,
21
// other filters...
22
};
23
24
const transactions = await fetchTransactions(filters, session.user.id);
25
26
return (
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";
2
3
import { useQueryState, parseAsString } from "nuqs";
4
import { useEffect } from "react";
5
import { useDebounce } from "use-debounce";
6
7
export default function SearchBar() {
8
const [search, setSearch] = useQueryState(
9
"search",
10
parseAsString.withDefault("").withOptions({ shallow: false })
11
);
12
13
const [inputValue, setInputValue] = useState(search);
14
const [debouncedValue] = useDebounce(inputValue, 250);
15
16
useEffect(() => {
17
startSearchTransition(() => {
18
setSearch(debouncedValue);
19
});
20
console.log(debouncedValue);
21
}, [debouncedValue, setSearch]);
22
23
return (
24
<input
25
type="text"
26
value={inputValue}
27
onChange={(e) => setInputValue(e.target.value)}
28
placeholder="Search transactions..."
29
className="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

  1. Keep filters/search params in the URL for shareability and SSR.
  2. Use nuqs for safe, typed query param parsing.
  3. Debounce inputs that trigger server refetches.
  4. Separate components:
    • Search/filter dropdowns → client
    • Data fetching & rendering → server
  5. 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.