03 Jan 2025 - 6 min read
In modern web applications, state management is often one of the trickiest challenges. Imagine you’re building a blog where users can filter posts by category, paginate through results, or search for specific content. Now, picture this: a user is browsing your blog, filters posts by category, and maybe even moves to a specific page in the pagination. But when the page refreshes, all that progress vanishes. The search term disappears, the selected category is reset, and the user has to start from scratch. It’s frustrating, right?
This problem, where state resets after a refresh or navigation, is common in many web applications. Developers often rely on client-side state management or even global stores, but these approaches come with their own set of challenges. Local state is ephemeral, tied to the lifecycle of a component. Global state is better but still fragile if not properly persisted. And using cookies or local storage can introduce complexity and SEO limitations. This is where query parameters come into play.
By encoding state directly into the URL, you make your application more resilient. With query parameters, states such as search filters, pagination settings, or even UI preferences like dark mode persist even after a page refresh. What's more, URLs with these parameters can be shared or bookmarked, ensuring users or search engines can always access the exact state of the application, improving both user experience and SEO.
Let’s take the example of a simple blog application where users can filter posts by category, search for keywords, and paginate through results. Normally, you’d handle state management using client-side tools like React’s useState or context providers, but these states would reset on refresh. By using query parameters, the state becomes part of the URL itself, which means the page can persist these settings across refreshes.
Here’s how this works in a Next.js environment. In Next.js, query parameters can easily be accessed and handled in server-side components, allowing us to fetch data based on the current state embedded in the URL. For example, a query URL like /posts/?page=2&category=tech
not only conveys the current page number but also the selected category. The app can then retrieve this state from the query parameters and render the appropriate content accordingly.
When a user visits /posts/?page=2&category=tech
, the URL contains all the necessary context — the page number and the category — which makes it easy to rehydrate the application state on the server side. The component responsible for fetching and displaying the posts can simply look for these parameters and adjust its logic to ensure the correct content is displayed.
In this setup, we have three key components: PostsPage
, TabBar
, and Pagination
.
Here’s how they come together:
interface SearchParams { page?: string; search?: string; category?: string; } const PostsPage = async ({ searchParams }: { searchParams: SearchParams }) => { const page = parseInt(searchParams.page as string, 10) || 1; const search = searchParams.search || ''; const categoryParam = convertToCategory(searchParams.category || 'WebDev'); return ( <div className="container mx-auto px-4 sm:px-16 lg:px-36 p-4"> <h1 className="text-3xl font-bold mb-6">Blog Posts</h1> <TabBar category={categoryParam} /> <Suspense fallback={<Spinner />}> <PostsList page={page} search={search} category={categoryParam} /> <Pagination page={page} category={categoryParam} /> </Suspense> </div> ); };
The PostsPage
component receives the query parameters like page
, search
, and category
, and adjusts the content accordingly. The TabBar
component handles category selection, and the Pagination
component updates the page number in the URL, maintaining the user's place.
Pagination is also easily handled with query parameters. Here’s how we can dynamically adjust the page number and category in the URL, ensuring that the user’s progress is preserved:
const Pagination: React.FC<PaginationProps> = async ({ page, category = 'webdev', }) => { const totalPages = await fetchTotalPages(category); return ( <div className="pagination"> <Link href={`/posts/?page=${page - 1}&category=${category}`}> Previous </Link> <p> Page {page} of {totalPages} </p> <Link href={`/posts/?page=${page + 1}&category=${category}`}> Next </Link> </div> ); };
This code is the part of my application. You can checkout the whole implementation here.
The main takeaway here is that query parameters provide a powerful, simple, and scalable way to manage state in web applications. Whether you're dealing with search, filtering, or pagination, leveraging the URL as a source of truth for your application’s state makes it more resilient and user-friendly. It’s a technique that works seamlessly in server-side rendered (SSR) frameworks like Next.js and is compatible with many other JavaScript libraries and frameworks. So, the next time you need to manage state, consider whether query parameters could be the elegant solution your app needs.