Languages
[Edit]
EN

TypeScript - detect and print changed HTML elements in DOM during React re-rendering (hydration validation)

5 points
Created by:
Marley-Marks
292

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.

React hydration on HTML returned from backend (with SSR / Server Side Rendering) - some data were loaded with AJAX and DOM changes were printed.
React hydration on HTML returned from backend (with SSR / Server Side Rendering) - some data were loaded with AJAX and DOM changes were printed.

 

 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
Native Advertising
🚀
Get your tech brand or product in front of software developers.
For more information Contact us
Dirask - we help you to
solve coding problems.
Ask question.

❤️💻 🙂

Join