279

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?
2

21 Answers 21

290

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.

Sign up to request clarification or add additional context in comments.

15 Comments

Is there a way to do this in IE? As it doesn't support =>
@joshuascotton yes there is! I'll try and add it in the answer here
@joshuacotton => is an arrow function, you can replace f => function fname(){...} with function(f){ return function fname(){...} }
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.
Using links to MDN as provided in this post, it is pretty clear that even in 2024 major browsers maybe should support the navigation API, but they simply don't support it. Of course, you can focus on Chrome and its clone Edge and you are good to go. However, if you intend to reach people running Firefox or using Apple hardware for whatever reason, you can't rely on the navigation API, not even in 2024.
|
148

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+

6 Comments

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 Any handler for full URL change(without anchor tag)?
You rarely need timeout events: use mouse- and keyboardevents for checking.
what if the path changes, not the hash?
what about if you want to monitor any new or update on the url GET params? thanks
|
105
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
});

6 Comments

This must be marked the answer. It's one line, uses browser event model and doesn't rely on endless resource consuming timeouts
Doesn't work on non-hash url changes which seems to be very popular such as the one implemented by Slack
How is this the best answer if this is only triggered when there is a hash in the url?
You should be using addEventListener instead of replacing the onhashchange value directly, in case something else wants to listen as well.
pageshow for user activated history navigation
|
83

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.

8 Comments

This worked for my use case - but just like @goat says - it's unbelievable that there's no native support for this...
what arguments? how would I set up fireEvents?
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).
this wont detect a change from localhost/foo to localhost/baa if not using location.back()
Unbelievable that we must resort to such hacks in 2022.
|
54

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

4 Comments

can I detect change of (window.location) and handle it? (without jquery)
You can @BergP, Using the plain javascript listener: window.addEventListener("hashchange", hashChanged);
Is such short time interval good for the app? That is, doesn't it keep the browser too busy in executing detect() function?
@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.
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  
}

2 Comments

pop state only triggers when you pop a state, not push one
This only works when navigating with the browsers back and forward buttons, ie completely useless in many cases.
22

for Chrome 102+ (2022-05-24)

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

API references WICG/navigation-api

3 Comments

Works like a charm! For Chrome this should be the marked answer as it catches all URL-changes!
can't believe I need scroll down this far for a updated answer!
Use it thoughtfully since it's in experimental phase so far developer.mozilla.org/en-US/docs/Web/API/Navigation/…
20

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

9 Comments

This is a rather rudimentary method, I think we can aim higher.
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.
@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."
will using setInterval really bring the browser to a halt? After how long?
Or instead of using setTimeout like @divibisan suggests, move the setInterval outside of the function. checkURLchange(); also becomes optional.
I agree that from all the answers this was the only way I was able to catch all of the URL changes.
|
10

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.

5 Comments

+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.
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.
Sure... It's still the general behavior, just not to spec. Seems you're using "pattern" too lightly. In JS, there are basically 4 or 5 ways to write a Singleton. If you use each of those across your codebase, are you really using the same pattern? One reason you may want to write patterns to spec: to make them more rapidly identifiable. Otherwise someone has to study it a bit to understand it implements the general behavior. Another reason: to avoid mistakes and so its functionality remains consistent across multiple implementations. Lastly, that quote is not exactly to spec :)
Where do you believe you can find a pattern "spec"? Gang of four? Can you find a place in the book where they say "Use our interface names exactly?". The book says they are just trying to organize the best solutions to common problems they've seen. Even the wikipedia page for observer pattern shows examples in Java, C++, Kotlin, Delphi, Python, C#, and JavaScript and only one of them uses the exact interface you mention. But to answer your question, yeah those 5 implementations of singleton pattern are exactly that. Although I recommend choosing one in your own project.
This can potentially be very slow. Observing the entire DOM just to check the location is not advised. (see this answer for more details - stackoverflow.com/a/39332340/1087372)
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
});

Comments

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

3 Comments

It's for nodeJs, we need to use browserify to use it client-side. Don't we?
No it isn't. Works in the browser
This didn't work for my project. It comes pre-bundled and makes a lot of assumptions about what bundler you are using.
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')
});

Comments

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

Comments

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.

4 Comments

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. stackoverflow.com/a/3452617/808901
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.
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
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.
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)
})

Comments

1

Enjoy!

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

Comments

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

2 Comments

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
If I was going to use something today for routes I would use crossroads.js
0

This will give you the new url

navigation.addEventListener('navigate',event)=>{
console.log("page changed", event.destination.url)
})

Comments

-1
window.addEventListener("beforeunload", function (e) {
    // do something
}, false);

2 Comments

this will not work in the context of single page applications since the unload event will never trigger
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.
-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.

1 Comment

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

2 Comments

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.
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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.