One thing to note about these two APIs is that they affect how the session history (the back/forward stack) behaves, but the global browser history (entries shown in the History tab) is separate.
Most browsers record every change in the global history regardless of whether `history.pushState` or `history.replaceState` is used. The HTML Spec[0] is explicit about session history but does not define how global history should behave.
I can understand why the spec makes no mention of this -- global history is a user-facing UI feature, similar to address bar autocomplete, and it makes sense for browsers to control this behavior. That said, I'm always annoyed when I look into my history tab after visiting a page like this (e.g. Vercel Domains[1]), and see my global history flooded with entries for each individual keystroke I've made, all in the name of "user experience".
In this particular case, it's just a fun gimmick, but for everyday websites I'd much prefer if they just debounced the updates to the URL to avoid cluttering the global history.
Thanks for the feedback, Vercel domain uses nuqs [1] (I'm the author) for URL state, and I agree flooding the browser history is a bad experience.
Is there a way to update the URL (ie: keeping it reactive in the address bar) without creating those history entries, or to ask the browser to squash the last entry it created into the previous one?
I am not aware of any approaches that work consistently across all major browsers. This matter is nothing new -- there's a Bugzilla report[0] from 13 years ago about this behavior that remains open.
Since there's no spec for global history and it's unlikely one will be introduced, the most practical solution to avoid flooding the browser history would be to debounce the changes.
This is the approach taken by Google Maps -- with maps being a well-known case where URL updates would clutter the history, as noted in the Bugzilla report.
function drawWorld() {
var hash = '#|' + gridString() + '|[score:' + currentScore() + ']';
if (urlRevealed) {
// Use the original game representation on the on-DOM view, as there are no
// escaping issues there.
$('#url').textContent = location.href.replace(/#.*$/, '') + hash;
}
// Modern browsers escape whitespace characters on the address bar URL for
// security reasons. In case this browser does that, replace the empty Braille
// character with a non-whitespace (and hopefully non-intrusive) symbol.
if (whitespaceReplacementChar) {
hash = hash.replace(/\u2800/g, whitespaceReplacementChar);
}
history.replaceState(null, null, hash);
// Some browsers have a rate limit on history.replaceState() calls, resulting
// in the URL not updating at all for a couple of seconds. In those cases,
// location.hash is updated directly, which is unfortunate, as it causes a new
// navigation entry to be created each time, effectively hijacking the user's
// back button.
if (decodeURIComponent(location.hash) !== hash) {
console.warn(
'history.replaceState() throttling detected. Using location.hash fallback'
);
location.hash = hash;
}
}