useSearchParams
in ReactURL state management is the practice of storing application state in the URL’s query parameters instead of component memory. useSearchParams
is a Hook that lets you read and update the query string in the browser’s URL, keeping your app’s state in sync with the address bar.
If you’re using the useState
Hook to manage filters or search parameters in your React app, you’re setting your users up for a frustrating experience. When a user refreshes the page, hits the back button, or tries to share a filtered view, all their selections vanish. That’s because the component state lives only in memory.
A better approach is to store the state in the URL. It keeps filters persistent, makes views shareable, and improves the user experience. Routing solutions like React Router and Next.js offer built-in ways to work with URL query parameters, making this approach straightforward to implement.
In this article, we’ll focus on React Router’s useSearchParams
Hook and show how to manage the state through the URL for a more resilient, user-friendly app.
To see the benefits of a URL-based state in action, we built a simple country explorer app with filters for name and region.
One version uses useState
, storing the filter state locally (see the useState
live demo here).
The other uses useSearchParams
, which stores the state in the URL (see the useSearchParams
demo here):
The difference is clear: one forgets your selections after a refresh or navigation, while the other remembers them and makes the view easily shareable. This subtle shift results in a far smoother, more reliable user experience.
Editor’s note: This post was updated by Ibadehin Mojeed in May 2025 to contrast useState
and useSearchParams
through the use of two demo projects and address FAQs around useSearchparams
.
useState
for managing filter stateIn the useState
version of our country explorer app, we manage the search query and region filter using the local component state:
const [search, setSearch] = useState(''); const [region, setRegion] = useState('');
Country data is fetched from the REST Countries API using React Router’s clientLoader
:
export async function clientLoader(): Promise<Country[]> { const res = await fetch('https://restcountries.com/v3.1/all'); const data = await res.json(); return data; }
While the method of filtering the data itself isn’t the focus here, how we store the filter state is. In this implementation, user input from the search and region fields is captured via onChange
handlers and stored locally using useState
:
<div className="flex flex-col sm:flex-row gap-4 mb-8"> <div className="relative w-full sm:w-1/2"> <input type="search" placeholder="Search by name..." value={search} onChange={(e) => setSearch(e.target.value)} // ... /> </div> <select value={region} onChange={(e) => setRegion(e.target.value)} // ... > </select> </div>
Because this filter state is stored only inside the component, it resets on page reload, can’t be bookmarked or shared, and isn’t accessible outside its local tree. That’s the key limitation of using useState
here.
useSearchParams
To address these limitations, we can move the filter state into the URL using query parameters. This approach, shown in the demo earlier, preserves filter settings across reloads, enables easy sharing via links, and greatly improves the user experience.
For example, after selecting a region and typing a country name, the URL might look like this:
https://use-search-params-silk.vercel.app/url-params?region=asia&search=vietnam
This URL encodes the app’s current filter state, making it easy to bookmark, share, or revisit later.
useSearchParam
for state managementReact Router’s useSearchParams
Hook lets us read and update URL query parameters (the part after the ?
). It behaves much like useState
, but instead of storing values in memory, it stores them directly in the URL. This makes the filter state stay persistent through reloads.
In our Country Explorer app, we use it like this:
const [searchParams, setSearchParams] = useSearchParams();
Here, searchParams
is an instance of the URLSearchParams
object reflecting the current query parameters in the URL. The setSearchParams
function updates these parameters, which in turn updates the URL and triggers navigation automatically.
To access filter values stored in the URL, we extract them using the searchParams
object like this:
const search = searchParams.get('search') || ''; const region = searchParams.get('region') || '';
Since URL parameters are always strings, it’s important to convert them to the appropriate types when needed. For example, to handle numbers or booleans, we can do:
const page = Number(searchParams.get('page') || 1); const showArchived = searchParams.get('showArchived') === 'true';
This ensures our app correctly interprets the parameters and maintains expected behavior.
To keep the URL in sync with user inputs, we update the query parameters inside their respective event handlers using setSearchParams
:
// Update search parameter const handleSearchChange = ( e: React.ChangeEvent<HTMLInputElement> ) => { const newSearch = e.target.value; setSearchParams((searchParams) => { if (newSearch) { searchParams.set('search', newSearch); } else { searchParams.delete('search'); } return searchParams; }); }; // Update region parameter const handleRegionChange = ( e: React.ChangeEvent<HTMLSelectElement> ) => { const newRegion = e.target.value; setSearchParams((searchParams) => { if (newRegion) { searchParams.set('region', newRegion); } else { searchParams.delete('region'); } return searchParams; }); };
setSearchParams
accepts a callback with the current searchParams
. Modifying and returning it updates the URL and triggers navigation automatically.
By default, every call to setSearchParams
adds a new entry to the browser’s history (e.g., when typing in a search box). This can clutter the back button behavior, making navigation confusing.
To prevent this, pass { replace: true }
as the second argument to setSearchParams
. This updates the URL without adding a new history entry, keeping back navigation clean and predictable:
setSearchParams( (searchParams) => { // ... }, { replace: true } );
This way, the URL stays in sync with the current filter state, while the browser history remains clean.
To avoid repeating setSearchParams
logic when managing multiple query parameters, we can encapsulate the update logic in a reusable helper function:
// Helper function for updating multiple params const updateParams = ( updates: Record<string, string | null>, replace = true ) => { setSearchParams( (searchParams) => { Object.entries(updates).forEach(([key, value]) => { value !== null ? searchParams.set(key, value) : searchParams.delete(key); }); return searchParams; }, { replace } ); };
This function centralizes setting, updating, and deleting parameters, keeping the code cleaner and easier to maintain. With updateParams
, we can pass an object of key-value pairs where null
values remove parameters from the URL.
With this helper, event handlers become concise:
const handleSearchChange = ( e: React.ChangeEvent<HTMLInputElement> ) => { updateParams({ search: e.target.value || null }); }; const handleRegionChange = ( e: React.ChangeEvent<HTMLSelectElement> ) => { updateParams({ region: e.target.value || null }); };
useSearchParams
useSearchParams
updating when using useNavigate
?setSearchParams
already handles navigation internally. It updates the URL and triggers a route transition. So, there’s no need to call useNavigate
separately.
useSearchParams
over window.location.search
?useSearchParams
provides a declarative way to manage query parameters within React. It keeps the UI in sync with the URL without triggering page reloads. In contrast, window.location.search
requires manual parsing, and updating it directly causes a full page reload, breaking the smooth experience expected in a single-page app (SPA).
Managing filter state with useState
may work at first, but as soon as users reload the page, use the back button, or try to share a specific view, its limitations become clear. That’s where useSearchParams
shines.
By syncing the UI state with the URL, we unlock persistence, shareability, and a smoother navigation experience. As demonstrated in the Country Explorer app, integrating query parameters with React Router is not only achievable but also leads to cleaner, more maintainable code and a more resilient user experience.
Whether you’re building filters, search, or pagination, managing state through the URL ensures your app behaves in a modern, reliable, and intuitive way.
If you found this guide helpful, consider sharing it with others who want to build better React experiences.
View the full project source code on GitHub.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowDiscover what’s new in Node.js 24, including major features, improvements, and how to prepare your projects.
Build agentic AI workflows with Ollama and React using local LLMs for enhanced privacy, reduced costs, and offline performance.
Learn when to choose monorepos or polyrepos for your frontend setup by comparing coordination, dependency management, CI/CD requirements, and more.
Today, we’ll be exploring the Open-Closed Principle: from the criticisms around it, its best use cases, and common misapplication.
5 Replies to "Why URL state matters: A guide to <code>useSearchParams</code> in React"
You can update search params easily with this approach, using native useSearchParams functional update.
setSearchParams((searchParams) => {
searchParams.set(“greeting”, “hi”);
return searchParams;
});
Hi,
Great post!
It would be nice to see a section on how to manage updating multiple keys at the same time.
For example if you set State A then State B you will end up with only State B’s changes.
In my case I am trying to update a start and end date that are updated by the same callback function.
Cheers,
Casey
The first example of useState calls setTotal instead of setGreeting
Thanks for the heads-up, this has been fixed
Created a library to simplify storing state in URL https://www.reddit.com/r/nextjs/comments/17d4x2k/about_using_url_for_managing_state_in_nextjs_13/