React versions

Quickly discover which web apps are running on the page and the React version they depend on.

Running multiple versions of React on the same page is inefficient and can increase load times. Use this tool to quickly spot outdated apps and identify which ones should be updated to reduce overhead.

Created
2025-09-30
Bookmarklet size
9.69 kB

Drag this link to your bookmarks to save it.

React versions

Bookmarklet code

react-versions

import createElement from '../../_lib/shared/create-element';
import { getInstance } from '../../_lib/shared/Dialog';
import { REG_APP_INFO, REG_REACT_SCRIPT_VERSION } from './patterns';
import { groupByVersionPrefix } from './utils';

(async (window, document) => {
if (!window.envision) {
console.warn('Envision not found. Exiting since most likely not Sitevision.');
return;
}

(await getInstance('react-versions-dialog', {
title: 'React versions',
size: 'large',
})
.init(() => {
const scripts = [...document.scripts];
const loadedVersions = scripts.map(s => REG_REACT_SCRIPT_VERSION.exec(s.src)?.[1].replaceAll('_', '.')).filter(v => !!v);
const pageApps = scripts
.filter(s => s.textContent.startsWith('AppRegistry.registerApp({'))
.map(s => REG_APP_INFO.exec(s.textContent)?.slice(1))
.filter(s => !!s)
.map(([ appId, appVersion, webAppAopId, reactVersion = '' ]) => ({ appId, appVersion, webAppAopId, reactVersion }));

const uniquePageApps = [...new Map(pageApps.map(a => [a.webAppAopId, a])).values()];
const groupedApps = groupByVersionPrefix(uniquePageApps, loadedVersions);
const container = createElement('<div></div>');

const versionsText = (() => {
switch (loadedVersions.length) {
case 0:
return "Could not find any React version at all, which is probably good.";
case 1:
return `Found one version of React (${loadedVersions[0]}) which is good. This means that you are not loading multiple versions, which would have resulted in a heavier page load.`;
default:
return `Found multiple versions of React (${loadedVersions.join(', ')}) which is bad. This most likely results in a heavier page load for the users.`;
}
})();
container.appendChild(createElement(`<p>${versionsText}</p>`));

let first = true;
for (const group of groupedApps) {
const { hasReactVersion } = group;
container.appendChild(createElement(`
<table class="env-table env-table--zebra env-table--small env-w--100
${first ? '' : 'env-m-top--large'}">
<caption>
${group.title}</caption>
<thead><tr><th>App identifier</th><th>App version</th>
${hasReactVersion ? '<th>React version</th>' : ''}</tr></thead>
<tbody>
${group.items.map(({ appId, appVersion, reactVersion }) =>
`<tr><td>${appId}</td><td>${appVersion}</td>${hasReactVersion ? `<td>${reactVersion}</td>` : ''}</tr>`).join('')}

</tbody>
</table>
`
));
first = false;
}

return container;
}))
.toggle();
})(window, window.document);

create-element.js

const DOCUMENT = window.document;
// const ENVISION = window.envision;

export default function createElement (markup) {
const template = DOCUMENT.createElement('template');
template.innerHTML = markup;

return template.content.firstElementChild;
}

Events.js

const CLICK = 'click';
const CHANGE = 'change';
const INPUT = 'input';
const KEYDOWN = 'keydown';
const SUBMIT = 'submit';
const PASTE = 'paste';

const add = (el, event, cb) => el.addEventListener(event, cb);
const remove = (el, event, cb) => el.removeEventListener(event, cb);

export default {
onClick: (el, cb) => add(el, CLICK, cb),
offClick: (el, cb) => remove(el, CLICK, cb),
onChange: (el, cb) => add(el, CHANGE, cb),
onInput: (el, cb) => add(el, INPUT, cb),
onKeydown: (el, cb) => add(el, KEYDOWN, cb),
onSubmit: (el, cb) => add(el, SUBMIT, cb),
onPaste: (el, cb) => add(el, PASTE, cb),
};

Dialog.js

import Events from "./Events";

const CLASS_BASE = 'env-modal-dialog';
const SPINNER_CLASS = 'env-spinner-bounce';
const DOCUMENT = window.document;
const ENVISION = window.envision;

/**
* Creates the HTML markup for an envision dialog.
*
* @param {object} options An object literal with options.
* @param {string} options.dialogId A unique html ID for the dialog.
* @param {string} options.title
* @returns {string}
*/


export class Dialog {
constructor ({ dialogId, title, size, closeText = 'Close' }) {
this.id = dialogId;
this.isLoading = false;
this.isDisabled = false;
this.title = title;
this.size = size;
this.closeText = closeText;
this.context = {};

this.tid = dialogId + '-t'; // title
this.thid = dialogId + '-th'; // title holder
this.cid = dialogId + '-c'; // content
this.eid = dialogId + '-e'; // error
this.fid = dialogId + '-f'; // footer
this.lid = dialogId + '-l'; // loader
}

/**
* Shortcut for Document.getElementById() for improved minification.
*
* @param {string} id An identifier for an HTMLElement.
* @returns {(HTMLElement|null)}
*/

el (id) {
return DOCUMENT.getElementById(id);
}

/**
* Initializes the dialog if it hasn't already been initialized.
*
* @param {(Function|String|HTMLElement)} initialContent
* @returns {Dialog} Returns self for chainability.
*/

async init (initialContent) {
const dialogId = this.id;
const titleId = this.tid;

if (this.el(dialogId)) {
return this;
}

let initialContentError;

try {
initialContent = typeof initialContent === 'function' ? await initialContent.call(null, this) : initialContent;
} catch (e) {
initialContentError = e;
}

const contentString = typeof initialContent === 'string' ? initialContent : '';
const sizeClass = this.size ? `${CLASS_BASE}__dialog--${this.size}` : '';

// Insert dialog into end of body and show dialog with envision.
DOCUMENT.body.insertAdjacentHTML('beforeend', (
`<div id="${dialogId}" class="${CLASS_BASE} ${CLASS_BASE}--inner-scroll" style="z-index:99999" role="dialog" aria-modal="true" aria-labelledby="${titleId}" aria-hidden="true" tabindex="-1">
<div class="
${CLASS_BASE}__dialog ${sizeClass}">
<section class="
${CLASS_BASE}__content">
<header class="
${CLASS_BASE}__header">
<h5 id="
${titleId}" class="env-text env-d--flex env-flex--align-items-center env-flex--justify-content-between ${CLASS_BASE}__header__title">
<span id="
${this.thid}">${this.title}</span>
<div id="
${this.lid}" class="${SPINNER_CLASS} ${SPINNER_CLASS}--hide ${SPINNER_CLASS}--fade-in" data-delay="medium">
${[1,2,3].map(i => `<div class="env-bounce${i}"></div>`).join('')}
</div>
</h5>
</header>
<div class="
${CLASS_BASE}__body">
<div id="
${this.cid}">${contentString}</div>
<div id="
${this.eid}" class="env-alert env-alert--danger" hidden>
<span data-error-holder></span>
<button type="button" class="env-alert__close env-button env-button--link env-button--icon env-button--icon-small" data-dismiss="alert">
Close
<svg class="env-icon env-icon--xx-small">
<use href="/sitevision/envision-icons.svg#icon-delete"></use>
</svg>
</button>
</div>
</div>
<footer id="
${this.fid}" class="${CLASS_BASE}__footer env-d--flex env-flex--justify-content-between">
<button type="button" data-modal-dialog-dismiss class="env-button env-m-left--a">
${this.closeText}</button>
</footer>
</section>
</div>
</div>
`

));

if (initialContentError) {
this.renderError(initialContentError);
}

Events.onClick(this.el(this.eid).querySelector('[data-dismiss="alert"]'), () => {
this.renderError('');
});

if (initialContent instanceof HTMLElement) {
this.el(this.cid).appendChild(initialContent);
}

return this;
}

isOpen () {
return this.el(this.id)?.classList.contains('env-modal-dialog--show');
}

/**
* @param {string} title
*/

updateTitle (title) {
this.title = title;
this.el(this.thid).innerHTML = title;
}

/**
* @param {string} html
*/

updateContent (html) {
this.el(this.cid).innerHTML = html;
}

/**
* @param {boolean} isLoading
*/

setLoading (isLoading) {
this.isLoading = !!isLoading;
this.el(this.lid).classList.toggle(`${SPINNER_CLASS}--hide`, !this.isLoading);
}

setDisabled (isDisabled) {
this.isDisabled = !!isDisabled;
this.el(this.id).inert = this.isDisabled;
this.el(this.cid).style.opacity = this.isDisabled ? 0.5 : 1;
this.el(this.fid).querySelectorAll('.env-button').forEach((btn) => btn.disabled = this.isDisabled);
}

/**
* @param {string} errorMessage
*/

renderError (errorMessage) {
const hasError = !!errorMessage;
const errorElement = this.el(this.eid);

errorElement.querySelector('[data-error-holder]').innerHTML = errorMessage;
errorElement.hidden = !hasError;

this.el(this.cid).hidden = hasError;
}

onClose (callback) {
if (typeof callback === 'function') {
// TODO: should probably start using new Dialog api instead of old ModalDialog so we can
// remove jQuery as a dependency.
jQuery('#' + this.id).on('hide.env-modal-dialog', (e) => {
callback.call(null, this, e);
});
}

return this;
}

onClosed (callback) {
if (typeof callback === 'function') {
// TODO: should probably start using new Dialog api instead of old ModalDialog so we can
// remove jQuery as a dependency.
jQuery('#' + this.id).on('hidden.env-modal-dialog', (e) => {
callback.call(null, this, e);
});
}

return this;
}

hide (callback) {
ENVISION.dialog('#' + this.id, 'hide').then(callback ? callback.bind(null, this) : null);
return this;
}

/**
* @param {Function} callback Callback run when Envision.dialog promise has finished.
* @returns {Dialog}
*/

toggle (callback) {
ENVISION.dialog('#' + this.id, 'toggle').then(callback ? callback.bind(null, this) : null);
return this;
}

remove () {
this.el(this.id).remove();
}
};

export default Dialog;

/**
* @returns {Record<string,Dialog>}
*/

export const getCache = (key = null) => {
const instances = window.__BookmarkletDialogInstances = window.__BookmarkletDialogInstances || {};

if (key) {
return instances[key];
}

return instances;
};

/**
* @returns {Dialog}
*/

export const getInstance = (dialogId, opts = {}) => {
const instances = getCache();

if (!instances[dialogId]) {
instances[dialogId] = new Dialog({ ...opts, dialogId });
}

return instances[dialogId];
};

export const removeInstance = (dialogId) => {
const instances = getCache();

if (instances[dialogId]) {
instances[dialogId].remove();
delete instances[dialogId];
}
};

patterns.js

export const REG_REACT_SCRIPT_VERSION = /webAppExternals\/react_(.*).js$/i;
export const REG_APP_INFO = /(?:webAppId:'(.*?)').*(?:webAppVersion:'(.*?)').*(?:webAppAopId:'(.*?)')(?:.*react":"(.*?)")?/i;

semver.js

/**
* Returns version as array of integers. Assumes no text suffix with hyphen exists in string.
*
* @param {string} versionString
* @
* @returns {number[]}
*/

export const parseVersion = (versionString) => versionString.split('.').map((part) => parseInt(part, 10));

/**
* Sort callback for comparing and sorting semver strings.
*
* @param {string} versionStringA
* @param {string} versionStringB
* @returns
*/

export const compareSemver = (versionStringA, versionStringB) => {
const pa = parseVersion(versionStringA);
const pb = parseVersion(versionStringB);
const n = Math.max(pa.length, pb.length);

for (let i = 0; i < n; i++) {
const na = pa[i] || 0;
const nb = pb[i] || 0;

if (na !== nb) {
return na - nb;
}
}

return 0;
};

utils.js

import { compareSemver } from "./semver";

/**
* Sorts array of semantic versions.
*
* @param {string[]} versions
* @returns {string[]}
*/

export const sortVersions = versions => [...versions].sort(compareSemver);

/**
* @typedef {Object} PageApp
* @property {string} appId
* @property {string} appVersion
* @property {string} webAppAopId
* @property {string} reactVersion
*/


/**
* @typedef {Object} VersionGroup
* @property {string} title
* @property {PageApp[]} items
* @property {boolean} hasReactVersion
*/


/**
* @param {PageApp[]} items
* @param {string[]} versions
* @returns {VersionGroup[]}
*/

export const groupByVersionPrefix = (items, versions) => {
const sortedVersions = sortVersions(versions);
const groups = {};
const unknownGroup = { title: 'Unknown version or not React', items: [], hasReactVersion: false, };

for (const item of items) {
const { reactVersion } = item;
const match = [...sortedVersions].reverse().find(version => reactVersion.startsWith(version));

if (match) {
if (!groups[match]) {
groups[match] = {
title: `React ${match}`,
version: match,
items: [],
hasReactVersion: true,
};
}

groups[match].items.push(item);
} else {
unknownGroup.items.push(item);
}
}

const resultGroups = Object.values(groups).sort((a, b) => {
const verA = a.version;
const verB = b.version;

return compareSemver(verA, verB);
});

if (unknownGroup.items.length > 0) {
resultGroups.push(unknownGroup);
}

return resultGroups;
};