MDN History API - discussion (2023-03-25)
The History API docs are one of those references you only appreciate after you've debugged a back/forward bug in production. It's not just about pushState—it's about deciding what state belongs in the URL, what belongs in history state, and how to restore posture without surprises.
Do you use history.state for UI posture (panel/tab/selection), or do you keep posture purely in the URL? How do you handle popstate restoration without re-running expensive work or re-triggering effects incorrectly? Do you log navigation transitions (push/replace/pop) as tips lines so back/forward bugs are explainable?
Comments (16)
Back to latestURL for shareable posture, history state for ephemeral posture.
Otherwise you end up encoding the whole app in query params.
We started logging history transitions and it paid off immediately:
txt
[tips] nav push path=/search?tag=hooks reason=userClick
[tips] nav replace path=/search?tag=hooks&page=2 reason=pagination
[tips] nav pop path=/search?tag=hooks reason=backButton restore=historyState
Before that, back/forward bugs were basically ghost stories.
Docs tie-in: the routing docs are basically a policy layer on top of History semantics.
Related: Routing and Navigation and Core Concepts.
Concrete alternative: keep everything in URL, never use history.state.
We tried that. It worked until we needed to restore scroll + selection without polluting share links. History state is fine if you treat it as a contract with evidence.
Yeah, I think the guardrail is: if it changes what someone expects from a link, it belongs in the URL.
If it's about restoration after a user action (scroll/selection), history state is a better fit.
Long-form: popstate restoration is where apps accidentally become non-idempotent.
If your route-enter logic kicks off fetches, effects, analytics, etc., then back/forward can double-trigger behavior. We had to make route enter replay-safe and log when we were replaying vs doing fresh work.
That is less about History itself and more about having a posture model that can be restored without side effects.
We only use replaceState for normalization (canonicalizing URLs) and never for user navigation.
Otherwise back/forward gets confusing because users can't return to what they just did.
We store scroll restoration in history state and treat it like a hint, not a guarantee.
If content identity changed, we log a fallback reason and don't pretend the scroll is exact.
A practical policy that worked for us:
txt
[tips] historyState allowedKeys=[scroll,selection] disallow=[filters,auth]
It forces you to decide what is restoration-only vs shareable posture.
Short take: if back/forward is buggy, your route-enter work is not replay-safe.
We found it helpful to render a small evidence token like nav:pop on the shell in dev builds.
It stops you from misattributing bugs to "random state" when it was actually a popstate restore path.
Long-form counterpoint: History state can become a hidden global store if you abuse it.
If teams start stuffing random objects in history.state, you lose the whole point of explicit contracts. Keep it tiny and log writes.
We standardized navigation transitions as actions: navPush, navReplace, navPopRestore.
Even if under the hood it's History API, naming it as actions kept it reviewable.
We added a tiny helper that prints a one-line tips log whenever we write to history.
It prevented accidental replaceState loops where the URL kept changing.
If you canonicalize URLs, do it once and make it converge.
If parse/serialize doesn't converge quickly, you built a navigation oscillator.
We treat history.state as the place for restoration hints only.
If it affects product behavior in a share link, it belongs in the URL.
Takeaway: the History API is simple; the policy decisions are not.
Write the policy down, log transitions, and back/forward stops being a mystery.