EN
TypeScript - detect and print changed HTML elements in DOM during React re-rendering (hydration validation for SSR)
5
points
In this short article, we would like to show how to detect changed DOM elements in React, when React re-renders.
Note: this tool is useful when we try to detect differences between React rendering results and with HTML returned from SSR - useful when hydration is used.

changesDetector.ts
file:
type ElementState = {
handle: Element;
backup: Element;
html: string;
children: Array<ElementState>
};
const renderNumber = (value: number, size: number): string =>
{
let text = String(value);
let zeros = '';
for (let i = text.length; i < size; ++i)
zeros += '0';
return zeros + text;
};
const formatDate = (date = new Date()) =>
{
const year = renderNumber(date.getFullYear(), 4);
const month = renderNumber(date.getMonth() + 1, 2);
const day = renderNumber(date.getDate(), 2);
const hours = renderNumber(date.getHours(), 2);
const minutes = renderNumber(date.getMinutes(), 2);
const seconds = renderNumber(date.getSeconds(), 2);
return `${year}.${month}.${day}_${hours}.${minutes}.${seconds}`;
};
const cloneElements = (hElement?: Element | null): ElementState | null =>
{
if (hElement)
{
const hBackup = hElement.cloneNode(true);
const children: Array<ElementState> = [];
const element: ElementState = {
handle: hElement,
backup: hBackup as Element,
html: hElement.outerHTML,
children: children
};
const hChildren = hElement.children;
for (let i = 0; i < hChildren.length; ++i)
{
const clone = cloneElements(hChildren[i]);
if (clone)
children.push(clone);
}
return element;
}
return null;
};
const compareElements = (previousElement?: ElementState | null, currentElement?: ElementState | null): boolean =>
{
if (previousElement)
{
if (!currentElement)
{
console.log(`-- element node was removed.`);
console.log(` [previous node]:`, previousElement.handle);
return false;
}
const aChildren = previousElement.children;
const bChildren = currentElement.children;
if (previousElement.handle !== currentElement.handle)
{
console.log(`-- element node was replaced.`);
console.log(` [previous node]:`, previousElement.backup);
console.log(` [current node]: `, currentElement.handle);
if (previousElement.html !== currentElement.html)
console.log(` [hint]: HTML changed`);
return false;
}
if (aChildren.length !== bChildren.length)
{
console.log(`-- children amount changed from ${aChildren.length} to ${bChildren.length}.`);
console.log(` [previous node]:`, previousElement.backup);
console.log(` [current node]: `, currentElement.handle);
if (previousElement.html !== currentElement.html)
console.log(` [hint]: HTML changed`);
return false;
}
let result = true;
for (let i = 0; i < aChildren.length; ++i)
{
if (!compareElements(aChildren[i], bChildren[i]))
result = false;
}
return result;
}
else
{
if (currentElement)
{
console.log(`-- element node was added.`);
console.log(` [current node]: `, currentElement.handle);
return false;
}
}
return true;
};
export const createChangesDetector = () =>
{
let handleInitialized: boolean = false;
let previousTime: string | null = null;
let previousBackup: ElementState | null = null;
return {
initialize: (handle?: Element | null): void =>
{
if (handleInitialized)
throw new Error('Changes detector already initialized.');
handleInitialized= true;
previousTime = formatDate();
previousBackup = cloneElements(handle);
},
compare: (handle?: Element | null): boolean =>
{
if (handleInitialized=== false)
throw new Error('Changes detector is not initialized.');
const currentTime = formatDate();
const currentBackup = cloneElements(handle);
console.log(`==============================================`);
console.log(` CHECKING ${currentTime}`);
console.log(`----------------------------------------------`);
const result = compareElements(previousBackup, currentBackup);
previousTime = currentTime;
previousBackup = currentBackup;
if (result)
console.log(`-- no changes.`);
console.log(`==============================================`);
return result;
}
};
};
Usage example on React:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { createChangesDetector } from './changesDetector';
const changesDetector = createChangesDetector();
const initializeDetector = () => changesDetector.initialize(document.querySelector('#root'));
const checkChanges = () => changesDetector.compare(document.querySelector('#root'));
initializeDetector(); // web site uses server side rendered html in div#root element
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
console.log('Initial rendering.');
setTimeout(checkChanges, 5000); // after 5s
setTimeout(checkChanges, 10000); // after 10s
// after ReactDOM.hydrate(...) call it shouldn't detect any changes if initial HTML is same