How to detect if URL has changed after hash in JavaScript
Asked Answered
F

21

261

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?
Forgat answered 17/6, 2011 at 18:45 Comment(2)
For future visitors, a new answer by @aljgom from 2018 is the best solution: https://mcmap.net/q/109106/-how-to-detect-if-url-has-changed-after-hash-in-javascriptBruit
This is a better answer using new constructs #53304019Carreon
P
146

In modern browsers (IE8+, FF3.6+, Chrome), you can just listen to the hashchange event on window.

In some old browsers, you need a timer that continually checks location.hash. If you're using jQuery, there is a plugin that does exactly that.

Example

Below I undo any URL change, to keep just the scrolling:

<script type="text/javascript">
  if (window.history) {
    var myOldUrl = window.location.href;
    window.addEventListener('hashchange', function(){
      window.history.pushState({}, null, myOldUrl);
    });
  }
</script>

Note that above used history-API is available in Chrome, Safari, Firefox 4+, and Internet Explorer 10pp4+

Personal answered 17/6, 2011 at 18:50 Comment(6)
This, as I understand, works only for the change of the part after the # sign (hence the event name)? And not for full URL change, as seems to be implied by the question's title.Vinnievinnitsa
@Vinnievinnitsa Any handler for full URL change(without anchor tag)?Dinin
You rarely need timeout events: use mouse- and keyboardevents for checking.Translunar
what if the path changes, not the hash?Kolo
@Kolo If the path changes because the user initiated a navigation / clicked a link etc., then you will only see a beforeunload event. If your code initiated the URL change, it knows best.Personal
what about if you want to monitor any new or update on the url GET params? thanksCloudless
U
258

Update 2024:

Major browsers should now support the Navigation API, so it can be done like this:

window.navigation.addEventListener("navigate", (event) => {
    console.log('location changed!');
})

API Reference: https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API


Previous Method (without the Navigation API):

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.

Upright answered 15/10, 2018 at 2:47 Comment(13)
Is there a way to do this in IE? As it doesn't support =>Gimlet
@Gimlet yes there is! I'll try and add it in the answer hereRobyn
@joshuacotton => is an arrow function, you can replace f => function fname(){...} with function(f){ return function fname(){...} }Upright
Your example works perfectly, all others on internet suggesting to use hashchange, but i don't use hash in my url, i just want to add listener for url change, thanks for sharing.Landin
Doesn't it send 'locationchange' twice then? Once when we fire it here, and once when the URL changes?Sudiesudnor
@Sudiesudnor 'locationchange' is a custom event, it doesn't exist without this code. It doesn't get fired by default with a url change without this modificationUpright
is there a way to do this conditionally 'if (window.location.href.indexOf("some_url") > -1)' location changed to some_url, then do an event?Kratzer
have to open my computer and log in StackOverflow to upvote this, in the middle of the nightGunmaker
Note: if hash is the only thing that changes it looks like locationchange event is not fired, only hashchange.Sandberg
@DominikSzymański It seems to work fine for me. When the hash is changed it triggers a popstate and then that triggers a locationchangeUpright
@Upright was hash the only thing that changed? What browser did you use?Sandberg
@DominikSzymański yes, Chrome. Make sure you are also running addEventListener('locationchange', ... )Upright
@Upright ah, I just understood the context. So the point of confusion is that I missed that locationchange is your custom event, that doesn't exist in browser APIs. What this answer misses then is possibly triggering hashchange event, that is triggered on other occasions where hash changes, but for some reason History API doesn't trigger it, and some other code might rely on it being triggered, because it's happening for e.g. link with href="#hash" or window.location.href='#hash' :)Sandberg
P
146

In modern browsers (IE8+, FF3.6+, Chrome), you can just listen to the hashchange event on window.

In some old browsers, you need a timer that continually checks location.hash. If you're using jQuery, there is a plugin that does exactly that.

Example

Below I undo any URL change, to keep just the scrolling:

<script type="text/javascript">
  if (window.history) {
    var myOldUrl = window.location.href;
    window.addEventListener('hashchange', function(){
      window.history.pushState({}, null, myOldUrl);
    });
  }
</script>

Note that above used history-API is available in Chrome, Safari, Firefox 4+, and Internet Explorer 10pp4+

Personal answered 17/6, 2011 at 18:50 Comment(6)
This, as I understand, works only for the change of the part after the # sign (hence the event name)? And not for full URL change, as seems to be implied by the question's title.Vinnievinnitsa
@Vinnievinnitsa Any handler for full URL change(without anchor tag)?Dinin
You rarely need timeout events: use mouse- and keyboardevents for checking.Translunar
what if the path changes, not the hash?Kolo
@Kolo If the path changes because the user initiated a navigation / clicked a link etc., then you will only see a beforeunload event. If your code initiated the URL change, it knows best.Personal
what about if you want to monitor any new or update on the url GET params? thanksCloudless
O
101
window.onhashchange = function() { 
     //code  
}

window.onpopstate = function() { 
     //code  
}

or

window.addEventListener('hashchange', function() { 
  //code  
});

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

with jQuery

$(window).bind('hashchange', function() {
     //code
});

$(window).bind('popstate', function() {
     //code
});
Outwash answered 7/5, 2016 at 17:18 Comment(6)
This must be marked the answer. It's one line, uses browser event model and doesn't rely on endless resource consuming timeoutsInadmissible
Doesn't work on non-hash url changes which seems to be very popular such as the one implemented by SlackKanter
How is this the best answer if this is only triggered when there is a hash in the url?Alimony
You should be using addEventListener instead of replacing the onhashchange value directly, in case something else wants to listen as well.Cuttlefish
pageshow for user activated history navigationLeralerch
@Alimony I would think that it's the best answer because that's exactly what OP asked for in his question.Sandberg
C
81

EDIT after a bit of researching:

It somehow seems that I have been fooled by the documentation present on Mozilla docs. The popstate event (and its callback function onpopstate) are not triggered whenever the pushState() or replaceState() are called in code. Therefore the original answer does not apply in all cases.

However there is a way to circumvent this by monkey-patching the functions according to @alpha123:

var pushState = history.pushState;
history.pushState = function () {
    pushState.apply(history, arguments);
    fireEvents('pushState', arguments);  // Some event-handling function
};

Original answer

Given that the title of this question is "How to detect URL change" the answer, when you want to know when the full path changes (and not just the hash anchor), is that you can listen for the popstate event:

window.onpopstate = function(event) {
  console.log("location: " + document.location + ", state: " + JSON.stringify(event.state));
};

Reference for popstate in Mozilla Docs

Currently (Jan 2017) there is support for popstate from 92% of browsers worldwide.

Charlatanism answered 24/1, 2017 at 9:56 Comment(8)
This worked for my use case - but just like @goat says - it's unbelievable that there's no native support for this...Turnover
what arguments? how would I set up fireEvents?Wagers
Note that this is also unreliable in many cases. For example, it won't detect the URL change when you click on different Amazon product variations (the tiles underneath the price).Oscular
this wont detect a change from localhost/foo to localhost/baa if not using location.back()Kolo
whats fireEvents ?Crankcase
Unbelievable that we must resort to such hacks in 2022.Stepha
I have a spa svelte with html5 routing this answer was the only answer that worked for me, because html5 routing is not hash-based routing so the hash event didn't work in my case, thank youBicephalous
If you have full control of the app it would be better to add a custom pushState function that does it and call it instead, rather than modifying the native function.Manteau
F
53

With jquery (and a plug-in) you can do

$(window).bind('hashchange', function() {
 /* things */
});

http://benalman.com/projects/jquery-hashchange-plugin/

Otherwise yes, you would have to use setInterval and check for a change in the hash event (window.location.hash)

Update! A simple draft

function hashHandler(){
    this.oldHash = window.location.hash;
    this.Check;

    var that = this;
    var detect = function(){
        if(that.oldHash!=window.location.hash){
            alert("HASH CHANGED - new has" + window.location.hash);
            that.oldHash = window.location.hash;
        }
    };
    this.Check = setInterval(function(){ detect() }, 100);
}

var hashDetection = new hashHandler();
Feinleib answered 17/6, 2011 at 18:49 Comment(4)
can I detect change of (window.location) and handle it? (without jquery)Misstep
You can @BergP, Using the plain javascript listener: window.addEventListener("hashchange", hashChanged);Supramolecular
Is such short time interval good for the app? That is, doesn't it keep the browser too busy in executing detect() function?Bradfordbradlee
@HasibMahmud, that code is doing 1 equality check every 100ms. I just benchmarked in my browser that I can do 500 equality checks in under 1ms. So that code is using 1/50000th of my processing power. I wouldn't worry too much.Cylix
F
28

Add a hash change event listener!

window.addEventListener('hashchange', function(e){console.log('hash changed')});

Or, to listen to all URL changes:

window.addEventListener('popstate', function(e){console.log('url changed')});

This is better than something like the code below because only one thing can exist in window.onhashchange and you'll possibly be overwriting someone else's code.

// Bad code example

window.onhashchange = function() { 
     // Code that overwrites whatever was previously in window.onhashchange  
}
Foreboding answered 1/6, 2017 at 23:26 Comment(2)
pop state only triggers when you pop a state, not push oneWagers
This only works when navigating with the browsers back and forward buttons, ie completely useless in many cases.Monophysite
C
22

for Chrome 102+ (2022-05-24)

navigation.addEventListener("navigate", e => {
  console.log(`navigate ->`,e.destination.url)
});

API references WICG/navigation-api

Capeskin answered 19/12, 2022 at 6:12 Comment(3)
Works like a charm! For Chrome this should be the marked answer as it catches all URL-changes!Ical
can't believe I need scroll down this far for a updated answer!Chittagong
Use it thoughtfully since it's in experimental phase so far developer.mozilla.org/en-US/docs/Web/API/Navigation/…Notice
R
19

this solution worked for me:

function checkURLchange(){
    if(window.location.href != oldURL){
        alert("url changed!");
        oldURL = window.location.href;
    }
}

var oldURL = window.location.href;
setInterval(checkURLchange, 1000);
Riptide answered 5/4, 2016 at 23:37 Comment(7)
This is a rather rudimentary method, I think we can aim higher.Gupta
Although I agree with @CarlesAlcolea that this feels old, in my experience it is still the only way to catch 100% of all url changes.Foreboding
@ahofmann suggests (in an edit that should have been a comment) changing setInterval to setTimeout: "using setInterval() will bring the Browser to a halt after a while, because it will create a new call to checkURLchange() every second. setTimeout() is the correct solition, because it is called only once."Heap
This has been the only solution to catch all the URL changes but the resource consuming is forcing me to seek some other solutions.Tensimeter
Or instead of using setTimeout like @Heap suggests, move the setInterval outside of the function. checkURLchange(); also becomes optional.Antedate
I agree that from all the answers this was the only way I was able to catch all of the URL changes.Frida
you can get rid of globals function checkURLchange(old_url){ var temp; temp=window.location.href; if(temp!=old_url){ fireChangeToBoard(); } old_url=temp; setTimeout(function(){ checkURLchange(old_url); }, 1000); }Lucilucia
F
11

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.

Fulford answered 20/3, 2021 at 15:35 Comment(2)
+1 for adding another approach. That said, MutationObserver definitely does not implement The Observer Pattern. The Observer Pattern looks like this... interface Observer { update(state: any): any } with a Subject ... interface Subject { attach(observer: Observer); detach(observer: Observer); notify() } ... rougly speaking. If a pattern has no consistency it is not a pattern.Barrator
That's not really true. There is consistency; just not in the naming. Getting hung up on the naming of the class's API is missing the forrest through the trees. The observer pattern is: "Define a one-to-many dependency between objects where a state change in one object results in all its dependents being notified and updated automatically." The implementation is not the pattern.Fulford
G
10

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

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

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

Update

The Navigation API is an experimental feature which may not be compatible with some browsers; see browser compatibility.

window.navigation.addEventListener('navigate', function() {
    // code
});
Gass answered 14/7, 2021 at 3:32 Comment(0)
S
4

Although an old question, the Location-bar project is very useful.

var LocationBar = require("location-bar");
var locationBar = new LocationBar();

// listen to all changes to the location bar
locationBar.onChange(function (path) {
  console.log("the current url is", path);
});

// listen to a specific change to location bar
// e.g. Backbone builds on top of this method to implement
// it's simple parametrized Backbone.Router
locationBar.route(/some\-regex/, function () {
  // only called when the current url matches the regex
});

locationBar.start({
  pushState: true
});

// update the address bar and add a new entry in browsers history
locationBar.update("/some/url?param=123");

// update the address bar but don't add the entry in history
locationBar.update("/some/url", {replace: true});

// update the address bar and call the `change` callback
locationBar.update("/some/url", {trigger: true});
Sealed answered 20/5, 2015 at 14:14 Comment(3)
It's for nodeJs, we need to use browserify to use it client-side. Don't we?Madison
No it isn't. Works in the browserSealed
This didn't work for my project. It comes pre-bundled and makes a lot of assumptions about what bundler you are using.Logical
A
4

To listen to url changes, see below:

window.onpopstate = function(event) {
  console.log("location: " + document.location + ", state: " + JSON.stringify(event.state));
};

Use this style if you intend to stop/remove listener after some certain condition.

window.addEventListener('popstate', function(e) {
   console.log('url changed')
});
Alver answered 20/12, 2017 at 9:1 Comment(0)
M
2

The answer below comes from here(with old javascript syntax(no arrow function, support IE 10+)): https://mcmap.net/q/109106/-how-to-detect-if-url-has-changed-after-hash-in-javascript

(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"));
  });
})();
Mcgee answered 21/8, 2019 at 8:6 Comment(0)
T
1

While doing a little chrome extension, I faced the same problem with an additionnal problem : Sometimes, the page change but not the URL.

For instance, just go to the Facebook Homepage, and click on the 'Home' button. You will reload the page but the URL won't change (one-page app style).

99% of the time, we are developping websites so we can get those events from Frameworks like Angular, React, Vue etc..

BUT, in my case of a Chrome extension (in Vanilla JS), I had to listen to an event that will trigger for each "page change", which can generally be caught by URL changed, but sometimes it doesn't.

My homemade solution was the following :

listen(window.history.length);
var oldLength = -1;
function listen(currentLength) {
  if (currentLength != oldLength) {
    // Do your stuff here
  }

  oldLength = window.history.length;
  setTimeout(function () {
    listen(window.history.length);
  }, 1000);
}

So basically the leoneckert solution, applied to window history, which will change when a page changes in a single page app.

Not rocket science, but cleanest solution I found, considering we are only checking an integer equality here, and not bigger objects or the whole DOM.

Thinnish answered 11/4, 2018 at 10:27 Comment(4)
You solution is simple and works very well for chrome extensions. I would like to suggest to use the YouTube video id instead of length. https://mcmap.net/q/25831/-how-do-i-get-the-youtube-video-id-from-a-urlMaren
you shouldn't use setInterval because each time you call listen(xy) a new Interval is created and you end up with thousands of intervals.Warr
You are right I noticed that after and didn't change my post, I will edit that. Back in the time I even encountered a crash of Google Chrome because of RAM leaks. Thank you for the commentThinnish
window.history has a max length of 50 (at least as of Chrome 80). After that point, window.history.length always returns 50. When that happens, this method will fail to recognize any changes.Melburn
L
1

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)
})
Logical answered 6/10, 2020 at 19:20 Comment(0)
S
1

I created this event that is very similar to the hashchange event

// onurlchange-event.js v1.0.1
(() => {
    const hasNativeEvent = Object.keys(window).includes('onurlchange')
    if (!hasNativeEvent) {
        let oldURL = location.href
        setInterval(() => {
            const newURL = location.href
            if (oldURL === newURL) {
                return
            }
            const urlChangeEvent = new CustomEvent('urlchange', {
                detail: {
                    oldURL,
                    newURL
                }
            })
            oldURL = newURL
            dispatchEvent(urlChangeEvent)
        }, 25)
        addEventListener('urlchange', event => {
            if (typeof(onurlchange) === 'function') {
                onurlchange(event)
            }
        })
    }
})()

Example of use:

window.onurlchange = event => {
    console.log(event)
    console.log(event.detail.oldURL)
    console.log(event.detail.newURL)
}

addEventListener('urlchange', event => {
    console.log(event)
    console.log(event.detail.oldURL)
    console.log(event.detail.newURL)
})
Standifer answered 3/11, 2020 at 14:33 Comment(2)
The code you wrote for the function callbacks is essentially what addEventListener and dispatchEvent do. You save callbacks using addEventListener(eventType, function), and when you dispatch an event, all the functions get calledUpright
If I was going to use something today for routes I would use crossroads.jsStandifer
P
0

Enjoy!

var previousUrl = '';
var observer = new MutationObserver(function(mutations) {
  if (location.href !== previousUrl) {
      previousUrl = location.href;
      console.log(`URL changed to ${location.href}`);
    }
});
Potiche answered 3/6, 2021 at 14:4 Comment(0)
B
0

This will give you the new url

navigation.addEventListener('navigate',event)=>{
console.log("page changed", event.destination.url)
})
Blouson answered 15/12, 2023 at 10:59 Comment(0)
O
-1
window.addEventListener("beforeunload", function (e) {
    // do something
}, false);
Odonto answered 1/5, 2016 at 12:14 Comment(2)
this will not work in the context of single page applications since the unload event will never triggerSanborne
The Question Author expressly stated concern about everything after the hash in the URL, talking about the hash-value alone. I don't even know WHAT this answer is. BeforeUnload is when location pathname or otherwise changes, limited to location.hash.Barrator
H
-1

Another simple way you could 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 the anchor tags must be in this format

<a href="#{page_name}">{Link title}</a>

then you could then use the "window.location.href" to get the url data that is the (#page_name) which you can send through ajax request to the server to get the html data of the requested page.

Then in the server side you could implement a switch statement in your favorite backend language to render each page respectively as requested by the client.

Simple and Easy.

Hekking answered 2/4, 2020 at 14:4 Comment(1)
I ...think I follow and I could see this being a useful strategy in certain contexts. That is, making only links of, say, a[data-navigable] capable of triggering navigation behavior. BTW, the backend wouldn't "render each page respectively" during an AJAX call unless you're using them as partials (but you could I suppose). I think most people would assume you just get data back in a hashchange/SPA type of environment. +1 for at least some inspiration.Barrator
M
-2

Look at the jQuery unload function. It handles all the things.

https://api.jquery.com/unload/

The unload event is sent to the window element when the user navigates away from the page. This could mean one of many things. The user could have clicked on a link to leave the page, or typed in a new URL in the address bar. The forward and back buttons will trigger the event. Closing the browser window will cause the event to be triggered. Even a page reload will first create an unload event.

$(window).unload(
    function(event) {
        alert("navigating");
    }
);
Martine answered 6/1, 2016 at 16:30 Comment(2)
Where content is Ajaxed in, the url may change without the window being unloaded. This script does not detect a url change, although it may still be helpful for some users who do have a window unload on every url change.Lemma
same as @Odonto question, this is specific only for pages that are not single page application, and this event is only watching the unload window event, not the url change.Sanborne

© 2022 - 2024 — McMap. All rights reserved.