Why URL state matters: A guide to useSearchParams in React

URL 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.

Why URL state matters: A guide to useSearchParams in React

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.

Why URL-based state management matters

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.


Limitations of useState for managing filter state

In 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.

URL-based State with 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.

Implementing useSearchParam for state management

React 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.

Retrieving state from URL

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.

Handling state updates

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.

Updating URL search params without adding to the history

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.

Updating multiple URL search parameters

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 });
};

Frequently asked questions about useSearchParams

Why isn’t 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.

What’s the benefit of 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).

Conclusion

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.

The post Why URL state matters: A guide to <code>useSearchParams</code> in React appeared first on LogRocket Blog.

 

This post first appeared on Read More