How to detect if URL has changed after hash in JavaScript

Viewed 352505

How can I check if a URL has changed in JavaScript? For example, websites like GitHub, which use AJAX, will append page information after a # symbol to create a unique URL without reloading the page. What is the best way to detect if this URL changes?

  • Is the onload event called again?
  • Is there an event handler for the URL?
  • Or must the URL be checked every second to detect a change?
20 Answers

I wanted to be able to add locationchange event listeners. After the modification below, we'll be able to do it, like this

window.addEventListener('locationchange', function () {
    console.log('location changed!');
});

In contrast, window.addEventListener('hashchange',() => {}) would only fire if the part after a hashtag in a url changes, and window.addEventListener('popstate',() => {}) doesn't always work.

This modification, similar to Christian's answer, modifies the history object to add some functionality.

By default, before these modifications, there's a popstate event, but there are no events for pushstate, and replacestate.

This modifies these three functions so that all fire a custom locationchange event for you to use, and also pushstate and replacestate events if you want to use those.

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'));
    });
})();

Note, we're creating a closure, to save the old function as part of the new one, so that it gets called whenever the new one is called.

None of these seem to work when a link is clicked that which redirects you to a different page on the same domain. Hence, I made my own solution:

let pathname = location.pathname;
window.addEventListener("click", function() {
    if (location.pathname != pathname) {
        pathname = location.pathname;
        // code
    }
});

Edit: You can also check for the popstate event (if a user goes back a page)

window.addEventListener("popstate", function() {
    // code
});

Best wishes,

Calculus

If none of the window events are working for you (as they aren't in my case), you can also use a MutationObserver that looks at the root element (non-recursively).

// capture the location at page load
let currentLocation = document.location.href;

const observer = new MutationObserver((mutationList) => {
  if (currentLocation !== document.location.href) {
    // location changed!
    currentLocation = document.location.href;

    // (do your event logic here)
  }
});

observer.observe(
  document.getElementById('root'),
  {
    childList: true,

    // important for performance
    subtree: false
  });

This may not always be feasible, but typically, if the URL changes, the root element's contents change as well.

I have not profiled, but theoretically this has less overhead than a timer because the Observer pattern is typically implemented so that it just loops through the subscriptions when a change occurs. We only added one subscription here. The timer on the other hand would have to check very frequently in order to ensure that the event was triggered immediately after URL change.

Also, this has a good chance of being more reliable than a timer since it eliminates timing issues.

The answer below comes from here(with old javascript syntax(no arrow function, support IE 10+)): https://stackoverflow.com/a/52809105/9168962

(function() {
  if (typeof window.CustomEvent === "function") return false; // If not IE
  function CustomEvent(event, params) {
    params = params || {bubbles: false, cancelable: false, detail: null};
    var evt = document.createEvent("CustomEvent");
    evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
    return evt;
  }
  window.CustomEvent = CustomEvent;
})();

(function() {
  history.pushState = function (f) {
    return function pushState() {
      var ret = f.apply(this, arguments);
      window.dispatchEvent(new CustomEvent("pushState"));
      window.dispatchEvent(new CustomEvent("locationchange"));
      return ret;
    };
  }(history.pushState);
  history.replaceState = function (f) {
    return function replaceState() {
      var ret = f.apply(this, arguments);
      window.dispatchEvent(new CustomEvent("replaceState"));
      window.dispatchEvent(new CustomEvent("locationchange"));
      return ret;
    };
  }(history.replaceState);
  window.addEventListener("popstate", function() {
    window.dispatchEvent(new CustomEvent("locationchange"));
  });
})();

Enjoy!

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

Found a working answer in a separate thread:

There's no one event that will always work, and monkey patching the pushState event is pretty hit or miss for most major SPAs.

So smart polling is what's worked best for me. You can add as many event types as you like, but these seem to be doing a really good job for me.

Written for TS, but easily modifiable:

const locationChangeEventType = "MY_APP-location-change";

// called on creation and every url change
export function observeUrlChanges(cb: (loc: Location) => any) {
  assertLocationChangeObserver();
  window.addEventListener(locationChangeEventType, () => cb(window.location));
  cb(window.location);
}

function assertLocationChangeObserver() {
  const state = window as any as { MY_APP_locationWatchSetup: any };
  if (state.MY_APP_locationWatchSetup) { return; }
  state.MY_APP_locationWatchSetup = true;

  let lastHref = location.href;

  ["popstate", "click", "keydown", "keyup", "touchstart", "touchend"].forEach((eventType) => {
    window.addEventListener(eventType, () => {
      requestAnimationFrame(() => {
        const currentHref = location.href;
        if (currentHref !== lastHref) {
          lastHref = currentHref;
          window.dispatchEvent(new Event(locationChangeEventType));
        }
      })
    })
  });
}

Usage

observeUrlChanges((loc) => {
  console.log(loc.href)
})

I created this, just add the function as an argument and whenever the link has any changes it will run the function returning the old and new url

I created this, just add the function as an argument and whenever the link has any changes it will run the function returning the old and new url

// on-url-change.js v1 (manual verification)
let onUrlChangeCallbacks = [];
let onUrlChangeTimestamp = new Date() * 1;
function onUrlChange(callback){
    onUrlChangeCallbacks.push(callback);
};
onUrlChangeAutorun();
function onUrlChangeAutorun(){
    let oldURL = window.location.href;
    setInterval(function(){
        let newURL = window.location.href;
        if(oldURL !== newURL){
            let event = {
                oldURL: oldURL,
                newURL: newURL,
                type: 'urlchange',
                timestamp: new Date() * 1 - onUrlChangeTimestamp
            };
            oldURL = newURL;
            for(let i = 0; i < onUrlChangeCallbacks.length; i++){
                onUrlChangeCallbacks[i](event);
            };
        };
    }, 25);
};

Another simple way you can do this is by adding a click event, through a class name to the anchor tags on the page to detect when it has been clicked, then you can now use the window.location.href to get the url data which you can use to run your ajax request to the server. Simple and Easy.

Related