BLOG

🌱 digital garden

Basé à Nice, France - CET (UTC+1)

BLOG

🌱 digital garden

Basé à Nice, France - CET (UTC+1)

BLOG

🌱 digital garden

Basé à Nice, France - CET (UTC+1)

Put the state in the URL

On data-heavy screens, most of what gets put in a client-side store is really the address of a view. The URL already stores that, for free.

The screens I work on are dashboards, portfolio tables and forecasting views: filters, sorting, date ranges, a selected row, a tab. The default way to build these today is to put all of it in a client-side store. I built several screens that way and kept writing the same synchronisation bugs. After a few rounds, my rule has become simple. If the user would expect it to survive a refresh, it belongs in the URL, and the store should never hear about it.

The test

For each piece of state, I ask three questions:

  1. If the user reloads, should it still be there?

  2. If they send the link to a colleague, should the colleague see the same thing?

  3. Should the back button undo it?

A filter, a sort order, a page number, an open tab, a selected record: yes to all three. A hover, an open dropdown, the text in a field before it is submitted: no. The first group is addressable state. The second is local, and useState is all it needs.

What is left for a global store after that split is small. Often it is the current user and nothing else.

What you get for free

Putting addressable state in the URL is not a style preference. It removes work:

  • Deep links. Support and sales stop describing how to reach a view in seven clicks. They paste a link.

  • A correct back button. History is an undo stack that the browser maintains, already tested by everyone.

  • Reload safety. There is no rehydration code, because nothing was ever only in memory.

  • A reproducible bug report. The URL in the ticket is the state. On a data screen that is most of the reproduction.

  • Tests without setup. An end-to-end test visits a URL and asserts. It does not click through filters to arrive.

Every one of these is a feature somebody eventually asks for. In a store-first design each is a project. In a URL-first design they were never missing.

One direction

The implementation that stays simple has a single rule: the URL is the source of truth, and components read from it.

user changes filter  →  update URL  →  parse URL  →  render
user changes filter  →  update URL  →  parse URL  →  render

There is no second copy to keep in sync. The bugs in this area almost always come from having the same value in the URL and in a store, and updating one of them.

A few details matter in practice:

  • Parse at the edge. Query strings are untrusted strings. One function turns them into a typed, validated object with defaults, and nothing downstream touches the raw string.

  • Leave defaults out. If the default sort is by date, the URL does not say so. Links stay short and a changed default does not break old links.

  • Push or replace, on purpose. Choosing a tab is a history entry. Each keystroke in a search box is not.

  • Treat the parameters as a public interface. People bookmark them. Renaming one is a breaking change, and the old name should keep working.

Where it does not fit

Not everything is an address. Anything sensitive stays out of the URL, since URLs end up in logs and in other people's chat windows. Very large state (a hand-picked selection of four hundred rows) does not fit either, and at that point it wants to be a saved object on the server with an identifier, which then goes in the URL. That is a better feature anyway.

Unsaved form input is the genuinely local case. It is not addressable, it should not survive a link, and it stays in the component.

The general point

I am wary of general points, but this is one I keep coming back to. A lot of front-end complexity comes from rebuilding things the platform already has. A store for view state is a reimplementation of the address bar, with more code and without the back button.

The web's own model is that a view has an address. Screens built on that model need less state management, mainly because there is less state left to manage.