Put That State in the URL
Filters, tabs and pagination belong in the address bar
5 min read

Here is a test you can run on any web app in under a minute, including your own.
Filter a list. Sort it. Go to page three. Now copy the address bar and send it to a colleague. When they open it, do they see what you saw?
For most applications, they see the unfiltered first page, because all of that state lived in component memory and the URL never changed. Then hit the back button. If it exits the entire page instead of undoing the filter, that is the same bug from a different angle.
This is one of the most common defects in modern frontends, and it is almost never filed as a defect. It just quietly makes the product feel worse than it is.
What the address bar actually is
The URL is a piece of persistent, shareable, user editable application state that browsers have supported forever, and that most single page applications stopped using when routing moved to the client.
When state lives there, four behaviours come free and require no code. Refresh preserves the view. Back and forward move through the user's own history of what they were looking at. Links are shareable, so a support agent can send a customer the exact filtered list. And bookmarks work, so a user who checks failed transactions every morning can save that view instead of reconstructing it daily.
When the same state lives in component memory, all four of those break at once, and you get bug reports that look unrelated to each other for months.
The rule that keeps it simple
The version people implement first is state in a hook plus an effect that syncs it to the URL, and another effect that syncs the URL back into state on navigation. That is two sources of truth held together by synchronisation code, and it will drift. Back navigation updates the URL but not the state, or a filter change writes twice and produces two history entries per click.
The rule that avoids all of it is to have no mirror. Read directly from the URL, write directly to the URL, and let the router be the only place the value lives.
Three details in there are worth naming, because they are what separates a working implementation from an irritating one.
Absent parameters mean defaults. When status is all, the key is deleted rather than written, which keeps URLs short and readable, and means a plain link is the same as an explicitly unfiltered one.
Changing a filter resets pagination. Without that line, a user on page five who narrows the filter lands on an empty page five of a two page result, sees nothing, and concludes there are no results.
Text input uses replace rather than push. If every keystroke pushes a history entry, back becomes useless, since the user has to press it fourteen times to escape a search box. Discrete actions like changing a tab or a page push. Continuous ones replace.
What does not belong there
This is a principle, not a religion, and pushing everything into the URL produces its own mess.
Ephemeral interface state stays local. Whether a dropdown is open, whether a tooltip is showing, the current hover target. Nobody wants to link to an open dropdown.
Anything sensitive stays out of it entirely. URLs end up in browser history, in server access logs, in analytics, and in the referer header sent to third parties. That is the wrong home for a token, an identifier that should not be enumerable, or anything personal.
Large state stays out for practical reasons. If your filter object is fifty fields, encoding it produces a URL that breaks when pasted into a chat app that wraps lines.
The useful boundary is this: if a user could reasonably want to return to this exact view later, or show it to someone, it goes in the URL. Otherwise it does not.
The part that pays off later
Beyond the user facing wins, there is an engineering benefit that shows up quietly.
State in the URL is state you can reason about from outside the application. A bug report becomes a link, and the link contains the reproduction. Support gets faster because a customer can send exactly what they are looking at. Analytics gets better because the parameters people actually use are already in your page views, so you can see which filters matter without instrumenting anything.
And on the implementation side, the entire class of synchronisation bugs disappears, because there is nothing to synchronise. One source of truth, read where it is needed, written when it changes.
Try it on one screen
You do not need to convert the whole application. Pick the busiest list view in your product, the one with filters people use daily, and move its state to the address bar this week.
Then run the test at the top again on that screen. Filter it, copy the link, open it in a private window, press back. When all four behaviours work, you will have fixed a set of small annoyances that your users never bothered reporting, because they assumed that is just how web applications work now.
It is not. It is how they work when the state ended up in the wrong place.