URLSearchparams provide the most safe way of managing state that ensures that your state is persistent, but in NextJS this means that your component must be dynamic since NextJS cannot know what these search params will be in advance. If you've ever tried using searchParams in a Next.js app and found yourself staring at hydration errors, missing query values, or strange crashes — you're definitely not alone. I’ve run into all these headaches and more. But once I started understanding how this hook works (and when it doesn’t), it completely changed how I handle state in the URL.
Table of Contents
- Why SearchParams are Great for State Management
- Mistake 1: Not Wrapping useSearchParams() in a Boundary
- Mistake 2: Using useSearchParams() in a Server Component
- Mistake 3: Assuming the Search Param Always Exists
- Mistake 4: Trying to Modify searchParams Directly
- Mistake 5: Forgetting to Encode/Decode Parameter Values
- ✅ Full Working Example
- Conclusion
Why SearchParams are Great for State Management
Before diving into the mistakes, let’s quickly talk about why URLSearchParams
(and by extension, useSearchParams()
) is such a solid tool for managing state:
- It persists across reloads: Since it's tied to the URL, your state survives full page refreshes without any extra setup.
- It creates shareable URLs: Users can copy and share the exact state of your page — filters, tabs, pagination, etc.
- It’s built-in: No need to bring in Redux, Zustand, or even
useState
for simple UI state. - Works great with shallow routing: Update the URL without reloading the entire page. Clean and efficient.
Now, let’s walk through the common mistakes you might encounter — and how to fix each one.
Mistake 1: Not Wrapping useSearchParams()
in a <Suspense> Boundary
One of the first cryptic errors I ran into was this:
Error: useSearchParams() should be wrapped in a suspense boundary
This confused me at first, but here's the deal: useSearchParams()
is asynchronous behind the scenes. That means Next.js expects it to be used inside a <Suspense>
boundary so it can resolve the data correctly during rendering.
✅ Fix: Wrap the component that uses useSearchParams()
in <Suspense>
:
import { Suspense } from 'react';
import { useSearchParams } from 'next/navigation';
function SearchComponent() {
const searchParams = useSearchParams();
const query = searchParams.get('q') || 'none';
return <div>Query: {query}</div>;
}
export default function Page() {
return (
<Suspense fallback=<div>Loading...</div>>
<SearchComponent />
</Suspense>
);
}
Mistake 2: Using useSearchParams()
in a Server Component
This one's a classic — you drop useSearchParams()
into a component and forget it's only available on the client. Boom! Server rendering error.
// ❌ This will break!
import { useSearchParams } from 'next/navigation';
export default function ServerComponent() {
const searchParams = useSearchParams(); // ❌ Invalid here
return <div>{searchParams.get('q')}</div>;
}
✅ Fix: Add 'use client'
at the top of your file to mark it as a client component:
'use client';
import { useSearchParams } from 'next/navigation';
export default function ClientComponent() {
const searchParams = useSearchParams();
return <div>{searchParams.get('q')}</div>;
}
That 'use client'
directive isn’t optional — it tells Next.js this component should only render in the browser.
Mistake 3: Assuming the Search Param Always Exists
This one tripped me up when I called .toLowerCase()
on a param that was actually null
.
const query = searchParams.get("query").toLowerCase(); // 💥 crash if null
✅ Fix: Always provide a fallback value, or check for null
explicitly:
const rawQuery = searchParams.get("query");
const query = typeof rawQuery === 'string' ? rawQuery.toLowerCase() : 'default';
Don’t trust the URL to always include what you expect — your app will thank you.
Mistake 4: Trying to Modify searchParams
Directly
The object returned from useSearchParams()
is readonly. You can’t just call set()
on it.
searchParams.set("filter", "active"); // ❌ this throws an error
✅ Fix: Create a new instance using URLSearchParams
and pass it to router.push()
:
const params = new URLSearchParams(searchParams.toString());
params.set("filter", "active");
router.push(`?${params.toString()}`);
Think of searchParams
like a snapshot — you’ll need to clone it if you want to make changes.
Mistake 5: Forgetting to Encode/Decode Parameter Values
If your query values include spaces or special characters, they’ll break your URLs unless you handle encoding properly.
params.set("name", "John Doe & Sons"); // ❌ results in broken query string
✅ Fix: Encode before setting, decode when reading:
params.set("name", encodeURIComponent("John Doe & Sons"));
...
const name = decodeURIComponent(searchParams.get("name") || "");
While URLSearchParams
will do some encoding for you automatically, it’s a good idea to be explicit, especially when dealing with user input.
✅ Full Working Example
'use client';
import React, { Suspense } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
function SearchComponent() {
const searchParams = useSearchParams();
const router = useRouter();
const query = searchParams.get("query") || "default";
const setParams = () => {
const params = new URLSearchParams(searchParams.toString());
params.set("query", "Hello World");
router.push(`?${params.toString()}`);
};
return (
<div>
<h1>Search Component</h1>
<p>Query: {query}</p>
<button onClick={setParams}>Set Query</button>
</div>
);
}
export default function Page() {
return (
<Suspense fallback=<div>Loading...</div>>
<SearchComponent />
</Suspense>
);
}
Conclusion
I’ve definitely made all of these mistakes — and probably a few more. But once you understand how useSearchParams()
works and how it fits into the Next.js rendering model, it becomes an incredibly useful tool.
Just remember:
- ✅ Wrap components with
<Suspense>
- ✅ Use
'use client'
when needed - ✅ Handle
null
safely - ✅ Don’t mutate
searchParams
directly - ✅ Encode your values when needed