How to know when a page redirect or refresh action is cancelled, Javascript

Viewed 716

I am currently working on a page where a preloader is shown to the user onPageNavigating, and when the user cancels the navigation from the browser, I would like to remove the preloader.

What i have tried.

  • Create a countDown timer
  • Start timer onPageNavigating
  • If timer elapse, assume navigation was cancelled
  • Hide preloader

But the problem with this method is that the time of navigation may vary based on network speed which makes it not feasible for my use-case.

What are other options to implement or is there an available browser API for this? because I can't find anything related via search.

EDIT:

Similar use-case where issue persists: For example visit: https://developer.android.com/guide or any link within its domain and notice the horizontal preloader at the top of the page. When you start navigation or refresh from the page you see the preloader shown but the challenge persist as I explained. Cancel the refresh immediately and the preloader would still be visible which isn't meant to be so.

Edit 2: Maybe rephrasing the question will help.

When i said onPageNavigating i actually mean Javascript's beforeunload event which is triggered when a user clicks on a link or initiates a page refresh.

window.addEventListener('beforeunload', function (e) {
  showPreloader();
  // ...At this point the preloader is visible
});

Because the beforeunload event can be cancelled. How can you tell when it is cancelled so the preloader can be hidden?

Imaginary event:

window.addEventListener('beforeunloadcancelled', function (e) {
      hidePreloader();
      // ...This is what i am looking for
    });
1 Answers

TL;DR

There is one approach to receive or emulate beforeunloadcancelled event in browsers using only browser APIs right now. But it doesn't work due to bugs in browsers which still haven't been fixed


Below I'll describe the main approach, why it doesn't work and what workarounds may be.

Approach based on browser APIs only

The main idea of emulating beforeunloadcancelled event is to get event that navigation request for next page has been cancelled by the user. You cannot control such kind of requests in the main browser context but, fortunately, you can do so from the service worker context.

If your service worker subscribes to fetch event, you will be able to work with all navigation requests your application makes:

// in Service Worker global scope
self.addEventListener('fetch', evt => {
    if (evt.request.mode === 'navigate') {
        console.log(`browser is navigating to the ${evt.request.url} resource now`);
    }
});

According to the specification each Request object has property signal. This signal represents a signal object that allows you to communicate with a DOM request (such as a Fetch) and abort it if required via an AbortController object.

You can subscribe to the abort event to get notified when request is aborted by the browser or via an AbortController. In that listener you can post message to the parent window with the information that navigation request has been canceled which actually means beforeunloadcancelled event:

// in Service Worker global scope
self.addEventListener('fetch', evt => {
    if (evt.request.mode === 'navigate') {
        console.log(`browser is navigating to the ${evt.request.url} resource now`);
        evt.request.signal.addEventListener('abort', async () => {
            // exit early if we don't have an access to the client (e.g. if it's cross-origin)
            if (!evt.clientId) return;

            // get the client
            const client = await self.clients.get(evt.clientId);
            // exit early if we don't get the client (e.g., if it's closed)
            if (!client) return;

            // send a message to the client
            client.postMessage({
                msg: 'navigation request has been canceled'
            });
        })
    }
});

Unfortunately, there are bugs that those abort events aren't reflected in service worker. Neither Chromium or Firefox have implemented properly sending the AbortSignal through to FetchEvent.request.signal. Please take a look:

So this approach doesn't work at the moment.

You can find more information on that topic here:

Possible workarounds

Detect of cancelled requests on the server side

If you have access to the server, you can detect that the navigation request has been cancelled on the server side.

Just send a notification to the client side in the event the navigation request is aborted. You can use WebSockets to send notifications. To make sure the notification is sent to the correct client some unique cookies based client Id may be used.

Timers based approaches

As you mentioned in the question, one possible workaround is to use some kind of timers:

window.addEventListener('beforeunload', () => {
    showPreloader();
    setTimeout(() => {
        hidePreloader(); // we're assuming navigation has been cancelled
    }, TIMEOUT);
});

To make this approach more precise you can choose the TIMEOUT value based on the current page load speed (which can be found in PerformanceNavigationTiming API):

const [initialNavigation] = window.performance.getEntriesByType('navigation');
const TIMEOUT = initialNavigation.duration * TIMEOUT_RATIO;

Track navigation request load duration with service worker

There is a way to track navigation request progress via service worker. You can send notification to the parent window when the navigation request is done. If navigation does not occur immediately after this notification, it means that navigation is effectively canceled and the preloader can be hidden:

// in Service Worker global scope
self.addEventListener('fetch', evt => {
    if (evt.request.mode === 'navigate') {
        console.log(`browser is navigating to the ${evt.request.url} resource now`);
        evt.respondWith(fetch(evt.request).then(response => {
            sendHidePreloaderNotification();
            return response;
        }));
    }
});

You can always combine last two options developing more stable approach.

Related