DEV Community

Nainik Mehta
Nainik Mehta

Posted on

Stop Using useState for Search, Filters, and Pagination

The Problem with Ephemeral State

In the early days of building interactive React dashboards, it was common practice to reach for useState to manage every piece of dynamic data. Need a search bar? useState. Filtering by date range? useState. Paginated data? useState. While this approach is intuitive and works perfectly for simple, isolated UI components, it creates a significant architectural bottleneck when applied to complex data-driven applications.

When you store your application's primary state—the state that dictates what data is displayed—in useState, you are essentially trapping that data in the browser's volatile memory. This results in an ephemeral user experience. If a user spends ten minutes meticulously configuring a dashboard, filtering by specific regions, and narrowing down a date range to find a critical insight, they expect that state to be persistent. If they refresh the page, or worse, copy the URL to share their findings with a colleague, they are met with a blank, default dashboard. The deep-link is dead.

The URL as the Single Source of Truth

The URL is one of the most powerful, underutilized features of the web. It is the only part of your application that is inherently shareable, bookmarkable, and persistent. By treating the URL as the single source of truth for your application's state, you align your software with the fundamental principles of the web.

When state is reflected in the URL query string, you are not just building a dashboard; you are building a document that can be referenced. This is the essence of modern React URL state management.

Implementing Type-Safe URL State with nuqs

Managing query parameters manually using URLSearchParams can quickly lead to a mess of boilerplate code, string parsing errors, and type-safety issues. This is where libraries like nuqs (formerly next-usequerystate) shine. When paired with TanStack Query, it provides a seamless, declarative way to sync your UI state with the browser address bar.

Code Example: Syncing State to the URL

Instead of managing state with useState, you can define your state hooks to synchronize directly with the URL query parameters:

import { useQueryState, parseAsInteger, parseAsString } from 'nuqs';

export function DashboardControls() {
  // Syncs with ?q=...
  const [search, setSearch] = useQueryState('q', parseAsString.withDefault(''));

  // Syncs with ?p=...
  const [page, setPage] = useQueryState('p', parseAsInteger.withDefault(1));

  return (
    <div>
      <input 
        value={search || ''} 
        onChange={(e) => setSearch(e.target.value)} 
        placeholder=\"Search...\"
      />
      <button onClick={() => setPage(page + 1)}>Next Page</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Why This Architecture Wins

Transitioning to URL-based state management offers three distinct engineering advantages:

1. Perfect Deep-Linking

By encoding the state in the URL, the link becomes a self-contained representation of the view. A user can share a URL, and the recipient will see the exact same filtered, paginated view without needing to perform a single click.

2. Unpolluted Browser History

Using router.replace for state updates—a common pattern when using URL state managers—prevents the "back button trap." Users can navigate through various filter combinations and still use the browser's back button to leave the page entirely, rather than having to click back through every single filter change.

3. Sync-Free Data Fetching

If you are using TanStack Query, you can use the URL parameters directly as your queryKey. Because the query key changes whenever the URL parameters change, the API fetch triggers automatically. You eliminate the need for useEffect hooks that manually listen for state changes to trigger data refetches, significantly reducing the surface area for bugs.

When to Use useState

This is not a call to banish useState entirely. useState is still the correct tool for truly local, transient UI states that do not affect the data being viewed. If the state is limited to hover effects, opening or closing a dropdown menu, or holding an unsubmitted draft in a form, useState is perfectly appropriate.

The rule of thumb is simple: If the user expects to refresh the page and see the same view, put it in the URL.

Conclusion

Moving away from useState for global dashboard state is a shift in mindset, but it is a necessary evolution for building professional-grade React applications. By leveraging the URL, you improve accessibility, shareability, and code maintainability. Start small—migrate your search or pagination state today and see how much cleaner your component logic becomes.

Top comments (1)

Collapse
 
raknaos profile image
Raknaos

The URL-as-source-of-truth switch is one of those refactors that quietly deletes a whole class of bugs. Ours was "the shared link shows different data" — every useState-plus-effect sync patch we tried left a race behind, and moving to the address bar as the canonical state was the only fix that held.

Two details I'd love your take on. Once the URL is canonical, every link pasted into Slack becomes a long-lived promise — do you version the params when a dashboard changes shape? And for search specifically, how do you handle the debounce: writing to the URL on every keystroke floods replaceState, but waiting too long makes the queryKey and the address bar visibly disagree on a mid-flight refresh.