248

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?
M. Justin
  • 14,487
  • 7
  • 91
  • 130
AJ00200
  • 16,327
  • 7
  • 23
  • 21
  • 1
    For future visitors, a new answer by @aljgom from 2018 is the best solution: https://stackoverflow.com/a/52809105/151503 – Redzarf Jun 27 '19 at 21:49
  • This is a better answer using new constructs https://stackoverflow.com/questions/53303519/detect-an-url-change-in-a-spa – Jahan Zinedine Jan 08 '23 at 16:00

21 Answers21

234

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.

aljgom
  • 7,879
  • 3
  • 33
  • 28
  • 1
    Is there a way to do this in IE? As it doesn't support => – joshuascotton Aug 15 '19 at 13:46
  • 1
    @joshuascotton yes there is! I'll try and add it in the answer here – Thomas Lang Aug 17 '19 at 20:05
  • 2
    @joshuacotton => is an arrow function, you can replace f => function fname(){...} with function(f){ return function fname(){...} } – aljgom Aug 26 '19 at 07:14
  • 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. – Haritsinh Gohil Mar 13 '20 at 10:35
  • Doesn't it send `'locationchange'` twice then? Once when we fire it here, and once when the URL changes? – JulienD Mar 26 '20 at 08:39
  • @JulienD '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 modification – aljgom Apr 25 '20 at 10:23
  • 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? – Prosy Arceno Mar 17 '23 at 07:49
144

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+

Top-Master
  • 7,611
  • 5
  • 39
  • 71
phihag
  • 278,196
  • 72
  • 453
  • 469
  • 158
    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. – NPC May 27 '14 at 21:53
  • 15
    @NPC Any handler for full URL change(without anchor tag)? – Neha Choudhary Dec 17 '14 at 07:50
  • You rarely need timeout events: use mouse- and keyboardevents for checking. – Sjeiti Jul 24 '19 at 04:58
  • 1
    what if the path changes, not the hash? – SuperUberDuper Dec 14 '19 at 09:28
  • @SuperUberDuper 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. – phihag Dec 14 '19 at 14:32
  • 1
    what about if you want to monitor any new or update on the url GET params? thanks – Alberto S. Mar 24 '20 at 12:25
100
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
});
Marc
  • 12,706
  • 7
  • 61
  • 97
Behnam
  • 6,244
  • 1
  • 39
  • 36
80

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.

Community
  • 1
  • 1
Cristian
  • 5,877
  • 6
  • 46
  • 55
  • 2
    This worked for my use case - but just like @goat says - it's unbelievable that there's no native support for this... – wasddd_ Nov 14 '18 at 18:33
  • 2
    what arguments? how would I set up fireEvents? – SeanMC Jan 30 '19 at 04:11
  • 3
    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). – thdoan May 05 '19 at 22:08
  • 3
    this wont detect a change from localhost/foo to localhost/baa if not using location.back() – SuperUberDuper Dec 14 '19 at 09:51
  • whats `fireEvents` ? – Gel Nov 11 '20 at 04:37
  • 1
    Unbelievable that we must resort to such hacks in 2022. – Kevin Farrugia Jan 21 '22 at 11:59
  • I have a spa svelte with [html5 routing](https://github.com/EmilTholin/svelte-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 you – Isaac Weingarten Apr 07 '22 at 04:58
  • 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. – Maciej Krawczyk Apr 30 '22 at 09:52
52

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();
cchana
  • 4,899
  • 3
  • 33
  • 43
Pantelis
  • 6,086
  • 1
  • 18
  • 21
  • can I detect change of (window.location) and handle it? (without jquery) – BergP Sep 13 '13 at 12:14
  • 8
    You can @BergP, Using the plain javascript listener: `window.addEventListener("hashchange", hashChanged);` – Ron Jul 03 '14 at 15:54
  • 1
    Is such short time interval good for the app? That is, doesn't it keep the browser too busy in executing `detect()` function? – Hasib Mahmud Aug 11 '14 at 15:05
  • 4
    @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. – Mark Bolusmjak Mar 16 '15 at 14:22
27

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  
}
Trev14
  • 3,626
  • 2
  • 31
  • 40
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);
aljgom
  • 7,879
  • 3
  • 33
  • 28
leoneckert
  • 299
  • 2
  • 2
  • 45
    This is a rather rudimentary method, I think we can aim higher. – Neithan Max Mar 11 '17 at 03:11
  • 15
    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. – Trev14 Aug 30 '17 at 22:57
  • 6
    @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." – divibisan May 31 '18 at 17:37
  • 1
    This has been the only solution to catch all the URL changes but the resource consuming is forcing me to seek some other solutions. – ReturnTable Jun 04 '18 at 18:55
  • 3
    Or instead of using `setTimeout` like @divibisan suggests, move the `setInterval` outside of the function. `checkURLchange();` also becomes optional. – aristidesfl Sep 04 '19 at 13:39
  • 3
    I agree that from all the answers this was the only way I was able to catch all of the URL changes. – Diego Fortes Jan 19 '20 at 03:53
  • 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); }` – ajax333221 Sep 26 '21 at 18:26
13

for Chrome 102+ (2022-05-24)

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

API references WICG/navigation-api

wener
  • 7,191
  • 6
  • 54
  • 78
9

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

Calculamatrise
  • 364
  • 3
  • 6
7

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.

Jared Beach
  • 2,635
  • 34
  • 38
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});
Ray Booysen
  • 28,894
  • 13
  • 84
  • 111
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')
});
codejockie
  • 9,020
  • 4
  • 40
  • 46
2

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"));
  });
})();
Yao Bao
  • 37
  • 3
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.

Marc.2377
  • 7,807
  • 7
  • 51
  • 95
Alburkerk
  • 1,564
  • 13
  • 19
  • 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://stackoverflow.com/a/3452617/808901 – Rod Lima Jan 02 '19 at 15:03
  • 2
    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. – Stef Chäser Mar 04 '19 at 09:05
  • 1
    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 comment – Alburkerk Mar 04 '19 at 10:21
  • 1
    `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. – Collin Krawll Mar 16 '20 at 22:11
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)
})
Seph Reed
  • 8,797
  • 11
  • 60
  • 125
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)
})
Luis Lobo
  • 489
  • 4
  • 7
  • 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 called – aljgom Dec 26 '21 at 04:10
  • If I was going to use something today for routes I would use crossroads.js – Luis Lobo Dec 26 '21 at 22:23
0

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");
    }
);
Kostiantyn
  • 1,792
  • 2
  • 16
  • 21
  • 3
    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. – Deborah May 20 '16 at 04:04
  • same as @ranbuch 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. – ncubica Jun 30 '16 at 17:25
0
window.addEventListener("beforeunload", function (e) {
    // do something
}, false);
ranbuch
  • 1,584
  • 16
  • 14
  • 13
    this will not work in the context of single page applications since the unload event will never trigger – ncubica Jun 30 '16 at 17:24
0

You are starting a new setInterval at each call, without cancelling the previous one - probably you only meant to have a setTimeout

Angelos Pikoulas
  • 1,002
  • 1
  • 14
  • 21
0

Enjoy!

var previousUrl = '';
var observer = new MutationObserver(function(mutations) {
  if (location.href !== previousUrl) {
      previousUrl = location.href;
      console.log(`URL changed to ${location.href}`);
    }
});
d-_-b
  • 21,536
  • 40
  • 150
  • 256
-2

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.

Benjamin
  • 133
  • 7