194

I'm writing a Greasemonkey script for a site which at some point modifies location.href.

How can I get an event (via window.addEventListener or something similar) when window.location.href changes on a page? I also need access to the DOM of the document pointing to the new/modified url.

I've seen other solutions which involve timeouts and polling, but I'd like to avoid that if possible.

1

14 Answers 14

206
+50

I use this script in my extension "Grab Any Media" and work fine ( like youtube case )

var oldHref = document.location.href;

window.onload = function() {
    var bodyList = document.querySelector("body")

    var observer = new MutationObserver(function(mutations) {
        if (oldHref != document.location.href) {
            oldHref = document.location.href;
            /* Changed ! your code here */
        }
    });
    
    var config = {
        childList: true,
        subtree: true
    };
    
    observer.observe(bodyList, config);
};

With the latest javascript specification

const observeUrlChange = () => {
  let oldHref = document.location.href;
  const body = document.querySelector("body");
  const observer = new MutationObserver(mutations => {
    if (oldHref !== document.location.href) {
      oldHref = document.location.href;
      /* Changed ! your code here */
    }
  });
  observer.observe(body, { childList: true, subtree: true });
};

window.onload = observeUrlChange;
11
  • 1
    The only solution that could have worked for youtube: [Report Only] Refused to create a worker from 'youtube.com/sw.js' because it violates the following Content Security Policy directive: "worker-src 'none'". Commented Apr 22, 2018 at 14:15
  • 9
    Worked out of the box with no trouble for my use case, God bless! It's annoying there's no native event for this yet (popstate did not work for me) but one day! I would use window.addEventListener("load", () => {}) instead of window.onload though :) Commented Mar 9, 2020 at 9:46
  • 2
    This is the smartest solution to this problem (listening for changes in the body). Also works within React to detect URL changes. Thanks a lot!
    – s.r.
    Commented Oct 24, 2021 at 19:44
  • 1
    put this in content script but it is not firing. Commented Mar 24, 2022 at 13:03
  • 2
    Hello I am new to using MutationObservers and love this solution,its the only one I found that worked sofar for YT redirects. Just wanted to note that you could make it more efficient by moving the if (oldHref != document.location.href) check before your forEach call as you would only need to do the check once.
    – SShah
    Commented Apr 30, 2022 at 11:34
119

popstate event:

The popstate event is fired when the active history entry changes. [...] The popstate event is only triggered by doing a browser action such as a click on the back button (or calling history.back() in JavaScript)

So, listening to popstate event and sending a popstate event when using history.pushState() should be enough to take action on href change:

window.addEventListener('popstate', listener);

const pushUrl = (href) => {
  history.pushState({}, '', href);
  window.dispatchEvent(new Event('popstate'));
};
6
  • 2
    But will the popstate fire before the content is loaded? Commented Mar 23, 2017 at 21:29
  • 35
    unusable if don't have any control over the piece of code that pushState Commented Feb 5, 2020 at 17:22
  • 7
    This does not always work, it relies on a popstate event to be fired. For example navigating within youtube it won't trigger, only if you press back or forward in history
    – TrySpace
    Commented May 27, 2020 at 8:37
  • this assume you have visibility or know if pushState is called.. it won't work everywhere, like comments above state as well
    – mikey
    Commented Feb 12, 2021 at 1:05
  • If you don't control the callers of history.pushState, then you can instead override history.pushState with a function that delegates to the original function, then calls your listener function. Commented Jan 22, 2023 at 13:08
43

You can't avoid polling, there isn't any event for href change.

Using intervals is quite light anyways if you don't go overboard. Checking the href every 50ms or so will not have any significant effect on performance if you're worried about that.

5
  • 4
    @AlexanderMills: fortunately there are these new solutions of popstate and hashchange.
    – serv-inc
    Commented Apr 24, 2017 at 20:07
  • 2
    This is not true.
    – inf3rno
    Commented Aug 18, 2018 at 12:42
  • @MartinZvarík Even in 2010 it was possible to use the unload event plus a micro or macro task to get the new document loaded.
    – inf3rno
    Commented Oct 17, 2018 at 2:06
  • 25
    Unfortunately this answer is still correct in 2020. popstate only fires when the user uses the forward/back buttons and hashchange only fires for hash changes. Unless you have control over the code that's causing the location to change, you have to poll 🤷‍♀️ Commented Apr 10, 2020 at 20:23
  • @RicoKahler yep, i tested and saw the same, popsate & hashchange don't work with some app frameworks, the url does change as seen but these 2 are not fired
    – Dan D.
    Commented May 8, 2023 at 9:59
40

In supporting browsers, use the new Navigation API which is currently being implemented as a replacement for the History API:

navigation.addEventListener('navigate', () => {
  console.log('page changed');
});

This already works in Chromium browsers, but Firefox and Safari are currently missing it as of January 2024.

It simplifies the problem considerably, and is being designed specifically with single page applications in mind which is the main sore spot with existing solutions. No more nasty expensive MutationObserver calls!

2
  • 3
    if you only ever use chrome Commented Nov 23, 2023 at 15:46
  • 1
    well it doesn't look that bad if you don't need Firefox and Safari :D
    – Dark
    Commented Mar 24 at 11:19
29

There is a default onhashchange event that you can use.

Documented HERE

And can be used like this:

function locationHashChanged( e ) {
    console.log( location.hash );
    console.log( e.oldURL, e.newURL );
    if ( location.hash === "#pageX" ) {
        pageX();
    }
}

window.onhashchange = locationHashChanged;

If the browser doesn't support oldURL and newURL you can bind it like this:

//let this snippet run before your hashChange event binding code
if( !window.HashChangeEvent )( function() {
    let lastURL = document.URL;
    window.addEventListener( "hashchange", function( event ) {
        Object.defineProperty( event, "oldURL", { enumerable: true, configurable: true, value: lastURL } );
        Object.defineProperty( event, "newURL", { enumerable: true, configurable: true, value: document.URL } );
        lastURL = document.URL;
    } );
} () );
2
  • 25
    Hashchange events are only sent if your URL has a part starting with a '#' symbol, normally used for anchor links. Otherwise it won't fire. Commented Dec 5, 2018 at 9:04
  • 2
    window.addEventListener("hashchange", funcRef, false); Commented Mar 9, 2020 at 9:45
14

Through Jquery, just try

$(window).on('beforeunload', function () {
    //your code goes here on location change 
});

By using javascript:

window.addEventListener("beforeunload", function (event) {
   //your code goes here on location change 
});

Refer Document : https://developer.mozilla.org/en-US/docs/Web/Events/beforeunload

2
  • 1
    Just try it, doesn't work on Firefox.
    – TGrif
    Commented Dec 31, 2021 at 21:46
  • it's working, use javascript Commented Jan 19, 2022 at 14:55
9

ReactJS and other SPA applications use the history object

You can listen to window.history updating with the following code:

function watchHistoryEvents() {
    const { pushState, replaceState } = window.history;

    window.history.pushState = function (...args) {
        pushState.apply(window.history, args);
        window.dispatchEvent(new Event('pushState'));
    };

    window.history.replaceState = function (...args) {
        replaceState.apply(window.history, args);
        window.dispatchEvent(new Event('replaceState'));
    };

    window.addEventListener('popstate', () => console.log('popstate event'));
    window.addEventListener('replaceState', () => console.log('replaceState event'));
    window.addEventListener('pushState', () => console.log('pushState event'));
}
watchHistoryEvents();
1
  • 1
    When use forward button of the browser this does not catch any event.
    – e-info128
    Commented Apr 6, 2023 at 2:28
8

Have you tried beforeUnload? This event fires immediately before the page responds to a navigation request, and this should include the modification of the href.

    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
    <HTML>
    <HEAD>
    <TITLE></TITLE>
    <META NAME="Generator" CONTENT="TextPad 4.6">
    <META NAME="Author" CONTENT="?">
    <META NAME="Keywords" CONTENT="?">
    <META NAME="Description" CONTENT="?">
    </HEAD>

         <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js" type="text/javascript"></script>
            <script type="text/javascript">
            $(document).ready(function(){
                $(window).unload(
                        function(event) {
                            alert("navigating");
                        }
                );
                $("#theButton").click(
                    function(event){
                        alert("Starting navigation");
                        window.location.href = "http://www.bbc.co.uk";
                    }
                );

            });
            </script>


    <BODY BGCOLOR="#FFFFFF" TEXT="#000000" LINK="#FF0000" VLINK="#800000" ALINK="#FF00FF" BACKGROUND="?">

        <button id="theButton">Click to navigate</button>

        <a href="http://www.google.co.uk"> Google</a>
    </BODY>
    </HTML>

Beware, however, that your event will fire whenever you navigate away from the page, whether this is because of the script, or somebody clicking on a link. Your real challenge, is detecting the different reasons for the event being fired. (If this is important to your logic)

5
  • 1
    I'm not sure if this will work because often the hash changes do no involve a page reload. Commented Aug 19, 2010 at 13:32
  • I need access to the new DOM as well, I updated the question to clarify that. Commented Aug 19, 2010 at 13:33
  • OK - didn't consider the hash situation - will think about that one. As for being able to access the new DOM, then you are out of luck (as far as accesssing this from an event handler is concerned) as the event will fire before the new DOM has been loaded. You might be able to incorporate the logic into the onload event of the new page, but you may have the same issues with respect to identifying whether you need to carry out the logic for every load of that page. Can you provide some more details about what you are trying to achieve, including page flow?
    – belugabob
    Commented Aug 19, 2010 at 13:38
  • 1
    I just tried accessing the location.href inside the onbeforeunload event handler, and it shows the original url, not the target url. Commented Feb 24, 2017 at 11:42
  • Beforeunload can cancel the unloading of the page, so the unload event is better, if you don't want to cancel navigation.
    – inf3rno
    Commented Aug 18, 2018 at 12:44
7

Try this script which will let you run code whenever the URL changes (without a pageload, like an Single Page Application):

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

based on the answer from "Leonardo Ciaccio", modified code is here: i.e. removed for loop and reassign the Body Element if it is removed

window.addEventListener("load", function () {
  let oldHref = document.location.href,
    bodyDOM = document.querySelector("body");
  function checkModifiedBody() {
    let tmp = document.querySelector("body");
    if (tmp != bodyDOM) {
      bodyDOM = tmp;
      observer.observe(bodyDOM, config);
    }
  }
  const observer = new MutationObserver(function (mutations) {
    if (oldHref != document.location.href) {
      oldHref = document.location.href;
      console.log("the location href is changed!");
      window.requestAnimationFrame(checkModifiedBody)
    }
  });
  const config = {
    childList: true,
    subtree: true
  };
  observer.observe(bodyDOM, config);
}, false);
2
  • Code-only answers are not particularly helpful. Please add some descriptions of how this code solves the problem. Commented Jun 17, 2021 at 7:43
  • This is a very complete answer!
    – Oytun
    Commented Dec 27, 2022 at 16:17
5

Also, I found a useful solution with MutationObserver:

function watchLocation() {
    const observable = () => document.location.pathname;

    let oldValue = observable();
    const observer = new MutationObserver(() => {
        const newValue = observable();

        if (oldValue !== newValue) {
            console.log(`changed: ${oldValue} -> ${newValue}`);
            oldValue = newValue;
        }
    });

    observer.observe(document.body, { childList: true, subtree: true });
}

MutationObserver API Documantation

1
  • Safari doesn't seem to honor the first call. Commented Nov 23, 2023 at 12:07
4

Well there is 2 ways to change the location.href. Either you can write location.href = "y.html", which reloads the page or can use the history API which does not reload the page. I experimented with the first a lot recently.

If you open a child window and capture the load of the child page from the parent window, then different browsers behave very differently. The only thing that is common, that they remove the old document and add a new one, so for example adding readystatechange or load event handlers to the old document does not have any effect. Most of the browsers remove the event handlers from the window object too, the only exception is Firefox. In Chrome with Karma runner and in Firefox you can capture the new document in the loading readyState if you use unload + next tick. So you can add for example a load event handler or a readystatechange event handler or just log that the browser is loading a page with a new URI. In Chrome with manual testing (probably GreaseMonkey too) and in Opera, PhantomJS, IE10, IE11 you cannot capture the new document in the loading state. In those browsers the unload + next tick calls the callback a few hundred msecs later than the load event of the page fires. The delay is typically 100 to 300 msecs, but opera simetime makes a 750 msec delay for next tick, which is scary. So if you want a consistent result in all browsers, then you do what you want to after the load event, but there is no guarantee the location won't be overridden before that.

var uuid = "win." + Math.random();
var timeOrigin = new Date();
var win = window.open("about:blank", uuid, "menubar=yes,location=yes,resizable=yes,scrollbars=yes,status=yes");


var callBacks = [];
var uglyHax = function (){
    var done = function (){
        uglyHax();
        callBacks.forEach(function (cb){
            cb();
        });
    };
    win.addEventListener("unload", function unloadListener(){
        win.removeEventListener("unload", unloadListener); // Firefox remembers, other browsers don't
        setTimeout(function (){
            // IE10, IE11, Opera, PhantomJS, Chrome has a complete new document at this point
            // Chrome on Karma, Firefox has a loading new document at this point
            win.document.readyState; // IE10 and IE11 sometimes fails if I don't access it twice, idk. how or why
            if (win.document.readyState === "complete")
                done();
            else
                win.addEventListener("load", function (){
                    setTimeout(done, 0);
                });
        }, 0);
    });
};
uglyHax();


callBacks.push(function (){
    console.log("cb", win.location.href, win.document.readyState);
    if (win.location.href !== "http://localhost:4444/y.html")
        win.location.href = "http://localhost:4444/y.html";
    else
        console.log("done");
});
win.location.href = "http://localhost:4444/x.html";

If you run your script only in Firefox, then you can use a simplified version and capture the document in a loading state, so for example a script on the loaded page cannot navigate away before you log the URI change:

var uuid = "win." + Math.random();
var timeOrigin = new Date();
var win = window.open("about:blank", uuid, "menubar=yes,location=yes,resizable=yes,scrollbars=yes,status=yes");


var callBacks = [];
win.addEventListener("unload", function unloadListener(){
    setTimeout(function (){
        callBacks.forEach(function (cb){
            cb();
        });
    }, 0);
});


callBacks.push(function (){
    console.log("cb", win.location.href, win.document.readyState);
    // be aware that the page is in loading readyState, 
    // so if you rewrite the location here, the actual page will be never loaded, just the new one
    if (win.location.href !== "http://localhost:4444/y.html")
        win.location.href = "http://localhost:4444/y.html";
    else
        console.log("done");
});
win.location.href = "http://localhost:4444/x.html";

If we are talking about single page applications which change the hash part of the URI, or use the history API, then you can use the hashchange and the popstate events of the window respectively. Those can capture even if you move in history back and forward until you stay on the same page. The document does not changes by those and the page is not really reloaded.

0

When dealing specifically with userscripts the methods can vary based on the script manager

Tampermonkey:

Tampermonkey supports the window.onurlchange event which can be enabled using the @grant directive. This allows you to directly listen to URL changes:

// ==UserScript==
// ...
// @grant        window.onurlchange
// ==/UserScript==

window.addEventListener('urlchange', (info) => ...);

documentation

Greasemonkey:

Unfortunately, Greasemonkey does not have a window.onurlchange feature. Instead, a general JavaScript method can be used.


General JavaScript Methods

Using the Navigation API (for modern browsers):

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

API Reference

Using Event Listeners:

Another approach involves overriding the state change functions of window.history to dispatch a locationchange event.

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

Then after running that code to do those modifications, event listeners can be used:

window.addEventListener('locationchange', () => {...})

This method uses Event Listeners, which are more efficient than polling or using Mutation Observers to detect changes across the entire DOM, both of which can lead to performance degradation.

I explain these approaches in more detail in this answer: How to detect if URL has changed after hash in JavaScript

-2

For React and React-router

import React, { useEffect } from 'react'
import { useLocation } from 'react-router-dom'
const App = () => {
   const { pathname } = useLocation();
   useEffect(() => {
      /* handle the event when pathname change */
  }, [pathname]);
}
2
  • As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.
    – Community Bot
    Commented Dec 14, 2023 at 17:25
  • where did the person who asked the question state they need with react router? please don't bring everything react just for the sake of answering Commented Jan 10 at 8:49

Not the answer you're looking for? Browse other questions tagged or ask your own question.