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:
xxxxxxxxxx
1
type ElementState = {
2
handle: Element;
3
backup: Element;
4
html: string;
5
children: Array<ElementState>
6
};
7
8
const renderNumber = (value: number, size: number): string =>
9
{
10
let text = String(value);
11
let zeros = '';
12
13
for (let i = text.length; i < size; ++i)
14
zeros += '0';
15
16
return zeros + text;
17
};
18
19
const formatDate = (date = new Date()) =>
20
{
21
const year = renderNumber(date.getFullYear(), 4);
22
const month = renderNumber(date.getMonth() + 1, 2);
23
const day = renderNumber(date.getDate(), 2);
24
const hours = renderNumber(date.getHours(), 2);
25
const minutes = renderNumber(date.getMinutes(), 2);
26
const seconds = renderNumber(date.getSeconds(), 2);
27
28
return `${year}.${month}.${day}_${hours}.${minutes}.${seconds}`;
29
};
30
31
const cloneElements = (hElement?: Element | null): ElementState | null =>
32
{
33
if (hElement)
34
{
35
const hBackup = hElement.cloneNode(true);
36
const children: Array<ElementState> = [];
37
38
const element: ElementState = {
39
handle: hElement,
40
backup: hBackup as Element,
41
html: hElement.outerHTML,
42
children: children
43
};
44
45
const hChildren = hElement.children;
46
47
for (let i = 0; i < hChildren.length; ++i)
48
{
49
const clone = cloneElements(hChildren[i]);
50
51
if (clone)
52
children.push(clone);
53
}
54
55
return element;
56
}
57
58
return null;
59
};
60
61
const compareElements = (previousElement?: ElementState | null, currentElement?: ElementState | null): boolean =>
62
{
63
if (previousElement)
64
{
65
if (!currentElement)
66
{
67
console.log(`-- element node was removed.`);
68
console.log(` [previous node]:`, previousElement.handle);
69
70
return false;
71
}
72
73
const aChildren = previousElement.children;
74
const bChildren = currentElement.children;
75
76
if (previousElement.handle !== currentElement.handle)
77
{
78
console.log(`-- element node was replaced.`);
79
console.log(` [previous node]:`, previousElement.backup);
80
console.log(` [current node]: `, currentElement.handle);
81
82
if (previousElement.html !== currentElement.html)
83
console.log(` [hint]: HTML changed`);
84
85
return false;
86
}
87
88
if (aChildren.length !== bChildren.length)
89
{
90
console.log(`-- children amount changed from ${aChildren.length} to ${bChildren.length}.`);
91
console.log(` [previous node]:`, previousElement.backup);
92
console.log(` [current node]: `, currentElement.handle);
93
94
if (previousElement.html !== currentElement.html)
95
console.log(` [hint]: HTML changed`);
96
97
return false;
98
}
99
100
let result = true;
101
102
for (let i = 0; i < aChildren.length; ++i)
103
{
104
if (!compareElements(aChildren[i], bChildren[i]))
105
result = false;
106
}
107
108
return result;
109
}
110
else
111
{
112
if (currentElement)
113
{
114
console.log(`-- element node was added.`);
115
console.log(` [current node]: `, currentElement.handle);
116
117
return false;
118
}
119
}
120
121
return true;
122
};
123
124
export const createChangesDetector = () =>
125
{
126
let handleInitialized: boolean = false;
127
let previousTime: string | null = null;
128
let previousBackup: ElementState | null = null;
129
130
return {
131
initialize: (handle?: Element | null): void =>
132
{
133
if (handleInitialized)
134
throw new Error('Changes detector already initialized.');
135
136
handleInitialized= true;
137
138
previousTime = formatDate();
139
previousBackup = cloneElements(handle);
140
},
141
compare: (handle?: Element | null): boolean =>
142
{
143
if (handleInitialized=== false)
144
throw new Error('Changes detector is not initialized.');
145
146
const currentTime = formatDate();
147
const currentBackup = cloneElements(handle);
148
149
console.log(`==============================================`);
150
console.log(` CHECKING ${currentTime}`);
151
console.log(`----------------------------------------------`);
152
153
const result = compareElements(previousBackup, currentBackup);
154
155
previousTime = currentTime;
156
previousBackup = currentBackup;
157
158
if (result)
159
console.log(`-- no changes.`);
160
161
console.log(`==============================================`);
162
163
return result;
164
}
165
};
166
};
Usage example on React:
xxxxxxxxxx
1
import React from 'react';
2
import ReactDOM from 'react-dom';
3
4
import './index.css';
5
6
import App from './App';
7
import { createChangesDetector } from './changesDetector';
8
9
const changesDetector = createChangesDetector();
10
11
const initializeDetector = () => changesDetector.initialize(document.querySelector('#root'));
12
const checkChanges = () => changesDetector.compare(document.querySelector('#root'));
13
14
initializeDetector(); // web site uses server side rendered html in div#root element
15
16
ReactDOM.render(
17
<React.StrictMode>
18
<App />
19
</React.StrictMode>,
20
document.getElementById('root')
21
);
22
console.log('Initial rendering.');
23
24
setTimeout(checkChanges, 5000); // after 5s
25
setTimeout(checkChanges, 10000); // after 10s
26
27
// after ReactDOM.hydrate(...) call it shouldn't detect any changes if initial HTML is same