Skip to content

Instantly share code, notes, and snippets.

@isocroft
Last active July 7, 2023 17:23
Show Gist options
  • Save isocroft/1d563ebe8f00c68e537e2e5083f44cca to your computer and use it in GitHub Desktop.
Save isocroft/1d563ebe8f00c68e537e2e5083f44cca to your computer and use it in GitHub Desktop.
Monkey-patch on the History interface
/**
* Algorithm {getNavDirection} created by @isocroft
*
* Copyright (c) 2021-2023 | Ifeora Okechukwu
*
* Works in ReactJS / VueJS / jQuery / Angular / Vanilla
*
* Algo implmented in Javascript: used to determine the direction
* of a single-page app navigation to track "Rage Refreshes" and to
* determine when the user visits a page from the back/forward button
* from a fresh page load.
*
* Using the code in the web link below:
* > https://www.codegrepper.com/code-examples/javascript/react+browser+back+button+event
*
* you can detect back/forward button navigations in ONLY ReactJS. How about if you could
* detect this in every single-page app library/framework with one code snippet ?
*/
// basic stack data-structure definition
class Stack {
constructor (data = []) {
this.length = 0
if (Array.isArray(data)) {
this.push.apply(this, data);
}
}
isEmpty () {
return this.length === 0;
}
size () {
return this.length;
}
peek () {
return this[this.size() - 1];
}
push (...args) {
return Array.prototype.push.apply(this, args)
}
pop () {
return Array.prototype.pop.call(this);
}
replaceTop (...args) {
this.pop();
this.push(...args);
}
toJSON () {
return '[ ' + Array.prototype.slice.call(this, 0).join(', ') + ' ]';
}
toObject () {
try {
return JSON.parse(this.toJSON())
} catch (e) {
if (e.name === 'SyntaxError') {
return Array.prototype.slice.call(this, 0, this.size())
}
return []
}
}
}
/* @NOTE: algorithm implementation of {getNavDirection} */
const getNavDirection = (navStack, lastLoadedURL) => {
/* @NOTE: Direction: back (-1), reload (0), fresh load (-9) and forward (1) */
let direction = -9;
/* @HINT: The current URL on browser page */
const docURL = document.location.href;
/* @HINT: The temporary "auxillary" stack object to aid page nav logic */
let auxStack = new Stack();
/* @HINT: Take note of the intial state of the navigation stack */
const wasNavStackEmpty = navStack.isEmpty();
// Firstly, we need to check that if the navStack isn't empty, then
// we need to remove the last-loaded URL to a temporary stack so we
// can compare the second-to-last URL in the stack with the current
// document URL to determine the direction
if(!wasNavStackEmpty) {
auxStack.push(
navStack.pop()
);
} else {
auxStack.push(docURL);
}
// Check top of the navigation stack (which is the second-to-last URL loaded)
// if it's equal to the currentg document URL. If it is, then the navigation
// direction is 'Back' (-1)
if (docURL === navStack.peek()) {
// Back (back button was clicked)
direction = -1;
} else {
// Check top of the temporary "auxillary" stack
if (lastLoadedURL === auxStack.peek()) {
// if the last-loaded URL is the
// current one and then determine
// the correct direction
if (lastLoadedURL === docURL) {
if (wasNavStackEmpty) {
direction = -9; // Fresh Load
} else {
direction = 0; // Reload (refresh button was clicked)
}
} else {
direction = 1; // Forward (forward button was clicked)
}
}
}
// If the direction is not 'Back' (i.e. back button clicked),
// then replace the URL that was poped earlier and optionally
// record the current document URL
if (direction !== -1) {
// if the temporary stack isn't empty
// then empty it's content into the
// top of the navigation stack
if(!auxStack.isEmpty()){
navStack.push(
auxStack.pop()
);
}
// push back the current document URL if and only if it's
// not already at the top of the navigation stack
if (docURL !== navStack.peek()) {
navStack.push(docURL);
}
}
// do away with the temporary stack (clean up action)
// as it's now empty
auxStack = null;
// return the direction of single-page app navigation
return direction; // Direction: back (-1), reload (0), fresh load (-9) and forward (1)
}
if (typeof window.History === 'function') {
// retrieve from storage (mostly for the refresh action of the browser)
const serializedStackFromStorage = window.sessionStorage.getItem('__nav_stack') || '[]'
const lastURLFromStorage = window.sessionStorage.getItem('__lastLoadedURL') || ''
// de-serialize the navigation stack from session storage
const deserializedStackFromStorage = JSON.parse(serializedStackFromStorage)
// monkey-patch the prototype object to include a navigation stack
window.History.prototype.spaNavigationStack = new Stack(deserializedStackFromStorage);
// monkey-patch the prototype object to include the last-loaded URL
window.History.prototype.lastLoadedURL = lastURLFromStorage || document.URL;
// Copy out the native browser `pushState` function for later use
var __pushState = window.History.prototype.pushState;
// Also, monkey-patch pushState (which is used by React / Vue / jQuery / Vanilla for their routing)
window.History.prototype.pushState = function (...args) {
const [, , url] = args
const origin = window.location.origin
const newURL = ((url.indexOf('http') === 0 ? url : origin + url) || '').toString();
const oldURL = document.URL;
const isProperNav = oldURL !== newURL;
if(isProperNav){
window.history.lastLoadedURL = newURL
window.sessionStorage.setItem('__lastLoadedURL', newURL)
}
return __pushState.apply(this, args);
};
}
const $onPopState = window.onpopstate;
// Setup the popstate event which spys on when only the
// browser back and forward button(s) are clicked
window.onpopstate = function(e) {
// Get the direction of the navigation
const navDirection = getNavDirection(
window.history.spaNavigationStack,
window.history.lastLoadedURL
);
window.sessionStorage.setItem(
'__nav_stack',
window.history.spaNavigationStack.toJSON()
)
// Save the direction to storage (optional)
window.sessionStorage.setItem(
'curr_page_nav_direction',
String(navDirection)
)
// Save refresh count in storage
if (navDirection === 0) {
const lastRefreshCount = window.sessionStorage.getItem('refresh_count') || '0'
window.sessionStorage.setItem(
'refresh_count',
String(parseInt(lastRefreshCount + 1))
)
}
// Trigger a custom event to fire
window.dispatchEvent(new CustomEvent(navDirection === -1 ? 'backwardNav' : 'navOccured'));
return typeof $onPopState === 'function' && typeof(e.isArtificial) === 'undefined' ? $onPopState(e) : undefined;
};
// detecting a refresh
if (window.history.lastLoadedURL === document.location.href) {
document.addEventListener('readystatechange', function (event){
if (event.readyState === 'complete') {
const artificialPopStateEvent = typeof(PopStateEvent) !== 'undefined' ? new PopStateEvent('popstate') : new Event('popstate')
artificialPopStateEvent.isArtificial = true
if (artificialPopStateEvent.constructor.name === 'Event') {
artificialPopStateEvent.state = null
}
/* @CHECK: https://docs.w3cub.com/dom/windoweventhandlers/onpopstate */
window.onpopsate(artificialPopStateEvent)
}
})
}
@isocroft
Copy link
Author

isocroft commented Jan 3, 2021

You can now listen for the custom event thus:

import { useEffect } from 'react';

useEffect(() => {

   const backNavhandler = (e) => {
      console.log('backwardNavigation: occured', Date.now());
   };

   window.addEventListener('backwardNav', backNavhandler, false);

   return () => {
         window.removeEventListener('backwardNav', backNavhandler);
   };

}, []);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment