28

Note

Solution should be in pure Javascript - No external library and frameworks.


In SPA, Everything gets routed using routing mechanism. I just want to listen to an even whenever any part of url changes (Not only hash. Any change I want to detect)

Following is example of SPA,

https://www.google.in/
https://www.google.in/women 
https://www.google.in/girl 

Now whenever url changes from https://www.hopscotch.in/ to https://www.hopscotch.in/women, I want to capture that event.

I tried,

window.addEventListener("hashchange",function(event){
    console.log(this); // this gets fired only when hash changes
});
Deepak Ingole
  • 14,912
  • 10
  • 47
  • 79

4 Answers4

29

In addition to Quentin's answer: setInterval is a pretty bad way to check for these changes: it's either never on time or gets fired too often.

What you really should do is watch for user-input events. Click is the most obvious one but don't forget about keyboard input. Should one of these events occur it will mostly just take one tick for the url to change. So in a quick mockup in code it's:

let url = location.href;
document.body.addEventListener('click', ()=>{
    requestAnimationFrame(()=>{
      url!==location.href&&console.log('url changed');
      url = location.href;
    });
}, true);

(just drop it into Github console to see it work)

Together with a popstate listener this should be enough for most use cases.

Sjeiti
  • 2,468
  • 1
  • 31
  • 33
  • It works well with `setTimeout` instead of `requestAnimationFrame` – allenhwkim Jul 19 '21 at 19:04
  • It might, just so as long you are aware of the dangers of using `setTimeout` in terms of timing. Plus `setTimeout` is also a delayed `eval`, which makes it somewhat evil. – Sjeiti Jul 20 '21 at 07:26
12

Under normal circumstances, there isn't an event when the URL changes. You are loading a new document (although you have load and so on)

If you are setting a new URL with JavaScript (i.e. with pushState) then there isn't an event, but you don't need one because you're already explicitly writing code around it, so you just add whatever else you need to that code.

You'll get a popstate event if the URL changes back though your pushState history via the browser back button or similar.


Consequently, there is no good generic way to hook into every SPA. The closest you could come would be to use setInterval and inspect the value of location.href to see if it changed since the last inspection.

Quentin
  • 914,110
  • 126
  • 1,211
  • 1,335
  • Thanks for the quick help. push and pop state wont work in my case. Will have to dig more to find what else can be done – Deepak Ingole Jun 07 '16 at 10:52
11

This script will let you detect changes to a URL within a SPA:

var previousUrl = '';
var observer = new MutationObserver(function(mutations) {
  if (location.href !== previousUrl) {
      previousUrl = location.href;
      console.log(`URL changed to ${location.href}`);
    }
});

const config = {subtree: true, childList: true};
observer.observe(document, config);

When you're done observing, be sure to cancel the observer, using the variable from the previous example:

observer.disconnect();
shaahiin
  • 1,243
  • 1
  • 16
  • 26
d-_-b
  • 21,536
  • 40
  • 150
  • 256
  • 1
    Just an addition, you also need to start observing via `observer.observe()` – Victor Fernandes Jul 20 '21 at 13:55
  • 7
    This gave me the following error: `Failed to execute 'observe' on 'MutationObserver': 1 argument required, but only 0 present.` – Mike Willis Aug 27 '21 at 18:25
  • @Sjeiti where and how would you remove this eventlistener? – Reena Verma Jun 22 '22 at 14:52
  • 1
    This is the only solution that works with the correct timing. A click listener with a delay will never be as precise as this solution. Maybe it can be slightly improved by assigning location.href to previousUrl already in the first line. `var previousUrl = location.href;` Also previousUrl can be declared with `let`, and the observer with `const`, since it observes but does not mutate (if you can use es6 syntax). – Giorgio Tempesta Jul 26 '22 at 07:20
  • After some time I figured out that this solution has an issue: when the URL changes, at least in the website I'm working on, the title is still the one of the previous page. This is leading to some issues in the GA tracking, so I changed the observed variable from the URL to the `document.title`, and in this case both URL and title have already changed – Giorgio Tempesta Oct 07 '22 at 10:49
0

I've implemented d-_-b's answer in my codebase, but I figured out that (at least in the website I'm working on) when the URL changed we still had the title from the previous page, so I changed it to listen to changes to the document.title instead of the location.href.

Here is my working solution:

let currentTitle = document.title;
const observer = new MutationObserver(function (mutations) {
  if (document.title !== currentTitle) {
    console.log('title changed', document.title);
    console.log('location also changed', document.location.href);
    // here you can add your own code
    currentTitle = document.title;
  }
});

const config = { subtree: true, childList: true };
observer.observe(document, config);

window.addEventListener('beforeunload', function (event) {
  observer.disconnect();
});
Giorgio Tempesta
  • 1,816
  • 24
  • 32