Update 2024:
Some modern browsers may now support the Navigation API, which can be used like this:
window.navigation.addEventListener("navigate", (event) => {
console.log('location changed!');
})
Navigation API documentation on MDN
Previous answer (Without the Navigation API):
After implementing the modifications detailed below, a custom locationchange event can be used, like this:
window.addEventListener('locationchange', function () {
console.log('location changed!');
});
Originally, before these modifications, there is only a popstate event, but there are no events for pushstate, and replacestate.
With these modifications, these history functions will also trigger a custom locationchange event, and also pushstate and replacestate events in case they're needed.
These are the modifications:
(() => {
let oldPushState = history.pushState;
history.pushState = function pushState() {
let ret = oldPushState.apply(this, arguments);
window.dispatchEvent(new Event('pushstate'));
window.dispatchEvent(new Event('locationchange'));
return ret;
};
let oldReplaceState = history.replaceState;
history.replaceState = function replaceState() {
let ret = oldReplaceState.apply(this, arguments);
window.dispatchEvent(new Event('replacestate'));
window.dispatchEvent(new Event('locationchange'));
return ret;
};
window.addEventListener('popstate', () => {
window.dispatchEvent(new Event('locationchange'));
});
})();
This modification, similar to Christian's answer, modifies the history object to add some functionality.
Note: A closure is being created, to save the old function as part of the new one, so that it gets called whenever the new one is called.
Notes on limitations of other solutions:
Using window.addEventListener('hashchange',() => {}) will only respond when the part after a hashtag in a url changes.
window.addEventListener('popstate',() => {}) is not always reliable for detecting all navigation changes because it only fires when navigating back or forward with the browser's buttons or similar methods. It does not trigger when the history is changed programmatically via history.pushState() or history.replaceState(), which are commonly used in single-page applications to update the URL without reloading the page.