why-hydration 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +394 -0
- package/dist/chunk-EPJM3LH3.cjs +717 -0
- package/dist/chunk-EPJM3LH3.cjs.map +1 -0
- package/dist/chunk-FV3PEJQE.js +42 -0
- package/dist/chunk-FV3PEJQE.js.map +1 -0
- package/dist/chunk-HYG6GVWO.js +16 -0
- package/dist/chunk-HYG6GVWO.js.map +1 -0
- package/dist/chunk-LNVWRW65.js +700 -0
- package/dist/chunk-LNVWRW65.js.map +1 -0
- package/dist/chunk-OGQEPU7G.cjs +49 -0
- package/dist/chunk-OGQEPU7G.cjs.map +1 -0
- package/dist/chunk-PVXOK7GJ.js +418 -0
- package/dist/chunk-PVXOK7GJ.js.map +1 -0
- package/dist/chunk-QGAEURQY.cjs +38 -0
- package/dist/chunk-QGAEURQY.cjs.map +1 -0
- package/dist/chunk-R6344ZUK.cjs +441 -0
- package/dist/chunk-R6344ZUK.cjs.map +1 -0
- package/dist/index.cjs +97 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +65 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/next/index.cjs +23 -0
- package/dist/next/index.cjs.map +1 -0
- package/dist/next/index.d.cts +4 -0
- package/dist/next/index.d.ts +4 -0
- package/dist/next/index.js +6 -0
- package/dist/next/index.js.map +1 -0
- package/dist/next/script.cjs +13 -0
- package/dist/next/script.cjs.map +1 -0
- package/dist/next/script.d.cts +9 -0
- package/dist/next/script.d.ts +9 -0
- package/dist/next/script.js +4 -0
- package/dist/next/script.js.map +1 -0
- package/dist/react.cjs +18 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +38 -0
- package/dist/react.d.ts +38 -0
- package/dist/react.js +5 -0
- package/dist/react.js.map +1 -0
- package/dist/types-DTlltJb9.d.cts +53 -0
- package/dist/types-DTlltJb9.d.ts +53 -0
- package/package.json +134 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Razan Aboushi (https://github.com/razan-aboushi)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
# why-hydration
|
|
2
|
+
|
|
3
|
+
**Tells you which component broke hydration, what differed, and how to fix it — in dev, with zero production cost.**
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<img src="docs/screenshots/overlay-hero.png" alt="why-hydration overlay showing three classified hydration mismatches" width="440">
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
React's hydration warnings tell you _that_ something mismatched, rarely _which
|
|
10
|
+
component_, _which value_, or _why_. `why-hydration` diffs the server DOM against
|
|
11
|
+
the client DOM, classifies the root cause, and hands you a specific fix.
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
▸ React: Warning: Text content did not match. Server: "١٢٣٤" Client: "1234"
|
|
15
|
+
|
|
16
|
+
▸ why-hydration: ⬡ locale-format (92%) — PriceTag · body > main > span:nth-child(2)
|
|
17
|
+
Server: ١٢٣٤ Client: 1234
|
|
18
|
+
Why: Same value, different digit scripts (Arabic-Indic vs Latin).
|
|
19
|
+
The server and client resolved to different locales.
|
|
20
|
+
Fix: Pass an explicit locale + timezone to Intl on both sides,
|
|
21
|
+
or format the value after mount.
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- 🔍 **Where** — component (best-effort) + exact DOM node + selector path.
|
|
25
|
+
- 🔀 **What** — server value vs client value, side by side.
|
|
26
|
+
- 🧠 **Why** — the cause, classified into a known category with a confidence score.
|
|
27
|
+
- 🛠️ **Fix** — a specific, actionable suggestion with a docs link.
|
|
28
|
+
- 🫧 **Zero prod cost** — everything is gated on `process.env.NODE_ENV` and
|
|
29
|
+
tree-shakes to a **no-op** (~0 B, size-budgeted in CI).
|
|
30
|
+
- 🌐 **Arabic-first** — digit-script mismatches (٠١٢ vs 012) are a first-class cause.
|
|
31
|
+
|
|
32
|
+
Output goes to an in-browser **overlay**, structured **console** output, and an
|
|
33
|
+
**`onReport`** callback so you can pipe it to your own logging. React 18 & 19.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm i -D why-hydration
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`react` and `react-dom` are peer dependencies (`^18 || ^19`).
|
|
44
|
+
|
|
45
|
+
## 30-second setup
|
|
46
|
+
|
|
47
|
+
### Next.js — App Router
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
// app/layout.tsx
|
|
51
|
+
import { HydrationSnapshotScript } from 'why-hydration/next/script';
|
|
52
|
+
import { HydrationInspector } from 'why-hydration/next';
|
|
53
|
+
|
|
54
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
55
|
+
return (
|
|
56
|
+
<html>
|
|
57
|
+
<head>
|
|
58
|
+
{process.env.NODE_ENV !== 'production' && <HydrationSnapshotScript />}
|
|
59
|
+
</head>
|
|
60
|
+
<body>
|
|
61
|
+
<HydrationInspector>{children}</HydrationInspector>
|
|
62
|
+
</body>
|
|
63
|
+
</html>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Next.js — Pages Router
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
// pages/_document.tsx → inside <Head>
|
|
72
|
+
import { HydrationSnapshotScript } from 'why-hydration/next/script';
|
|
73
|
+
// ...
|
|
74
|
+
<Head>{process.env.NODE_ENV !== 'production' && <HydrationSnapshotScript />}</Head>
|
|
75
|
+
|
|
76
|
+
// pages/_app.tsx
|
|
77
|
+
import { HydrationInspector } from 'why-hydration/next';
|
|
78
|
+
export default function App({ Component, pageProps }) {
|
|
79
|
+
return (
|
|
80
|
+
<HydrationInspector>
|
|
81
|
+
<Component {...pageProps} />
|
|
82
|
+
</HydrationInspector>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Vite / CRA / Remix (you own `hydrateRoot`)
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
import { hydrateRoot } from 'react-dom/client';
|
|
91
|
+
import { createHydrationInspector } from 'why-hydration/react';
|
|
92
|
+
|
|
93
|
+
const inspector = createHydrationInspector();
|
|
94
|
+
|
|
95
|
+
hydrateRoot(document.getElementById('root')!, <App />, {
|
|
96
|
+
onRecoverableError: inspector.onRecoverableError,
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Add the snapshot script to your HTML `<head>` so the server DOM is captured
|
|
101
|
+
before hydration (Vite `index.html`, before your entry `<script>`). Copy-paste:
|
|
102
|
+
|
|
103
|
+
```html
|
|
104
|
+
<head>
|
|
105
|
+
<!-- why-hydration snapshot — dev only, runs before hydration -->
|
|
106
|
+
<script>
|
|
107
|
+
(function () {
|
|
108
|
+
var K = '__WHY_HYDRATION_SNAPSHOT__';
|
|
109
|
+
if (typeof window === 'undefined' || window[K]) return;
|
|
110
|
+
var S = ['#root'];
|
|
111
|
+
function cap() {
|
|
112
|
+
if (window[K]) return;
|
|
113
|
+
var r = {};
|
|
114
|
+
for (var i = 0; i < S.length; i++) {
|
|
115
|
+
var el = document.querySelector(S[i]);
|
|
116
|
+
if (el) r[S[i]] = el.innerHTML;
|
|
117
|
+
}
|
|
118
|
+
window[K] = { version: 1, capturedAt: Date.now(), roots: r };
|
|
119
|
+
}
|
|
120
|
+
if (document.readyState !== 'loading') cap();
|
|
121
|
+
else {
|
|
122
|
+
document.addEventListener('readystatechange', function () {
|
|
123
|
+
if (document.readyState === 'interactive') cap();
|
|
124
|
+
});
|
|
125
|
+
document.addEventListener('DOMContentLoaded', cap);
|
|
126
|
+
}
|
|
127
|
+
})();
|
|
128
|
+
</script>
|
|
129
|
+
</head>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
> Prefer to generate it? `import { getSnapshotScriptSource } from 'why-hydration'`
|
|
133
|
+
> returns exactly this string. Without the snapshot script the tool still works
|
|
134
|
+
> from React's console warnings, but the precise DOM value/attribute diff needs
|
|
135
|
+
> it.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## What you'll see
|
|
140
|
+
|
|
141
|
+
When a mismatch happens in dev, you get three things (all off in production):
|
|
142
|
+
|
|
143
|
+
1. **An overlay** in the corner — one card per mismatch with the category,
|
|
144
|
+
confidence, server vs client values, why it happened, and the fix. Dismissible.
|
|
145
|
+
2. **A grouped console block** — the same info, easy to copy into an issue.
|
|
146
|
+
3. **Your `onReport` callback** (if provided) — the structured `HydrationReport`.
|
|
147
|
+
|
|
148
|
+
A clean page with no mismatches shows **nothing** — no overlay, no logs.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Options
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
<HydrationInspector
|
|
156
|
+
overlay // boolean | { position: 'bottom-right' | ... } (default: on in dev)
|
|
157
|
+
onReport={(report) => sendToLogging(report)}
|
|
158
|
+
ignore={['.grammarly-ext', (node) => node.hasAttribute('data-safe')]}
|
|
159
|
+
classify={[myCustomRule]} // extra classifiers, run before the built-ins
|
|
160
|
+
maxReports={25}
|
|
161
|
+
>
|
|
162
|
+
{children}
|
|
163
|
+
</HydrationInspector>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
| Option | Type | Default | Description |
|
|
167
|
+
| ------------ | --------------------------------------------------- | -------------- | -------------------------------------------------------- |
|
|
168
|
+
| `overlay` | `boolean \| OverlayOptions` | `true` in dev | In-browser panel. `false` to disable. |
|
|
169
|
+
| `onReport` | `(report: HydrationReport) => void` | – | Called once per unique report. |
|
|
170
|
+
| `ignore` | `Array<string \| (node: Element) => boolean>` | `[]` | Suppress known-safe mismatches by selector or predicate. |
|
|
171
|
+
| `classify` | `Classifier[]` | `[]` | Custom rules, run **before** the built-ins. |
|
|
172
|
+
| `maxReports` | `number` | `25` | Cap on unique reports. |
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## The `HydrationReport`
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
interface HydrationReport {
|
|
180
|
+
id: string;
|
|
181
|
+
timestamp: number;
|
|
182
|
+
component?: string;
|
|
183
|
+
componentStack?: string;
|
|
184
|
+
location?: { file?: string; line?: number; column?: number };
|
|
185
|
+
node: {
|
|
186
|
+
path: string; // selector-style path to the node
|
|
187
|
+
tagName?: string;
|
|
188
|
+
attribute?: string;
|
|
189
|
+
kind: 'text' | 'attribute' | 'structure' | 'node-added' | 'node-removed';
|
|
190
|
+
};
|
|
191
|
+
server: string | null;
|
|
192
|
+
client: string | null;
|
|
193
|
+
cause: {
|
|
194
|
+
category: HydrationCauseCategory;
|
|
195
|
+
confidence: number; // 0–1
|
|
196
|
+
explanation: string;
|
|
197
|
+
suggestion: string;
|
|
198
|
+
docsUrl?: string;
|
|
199
|
+
};
|
|
200
|
+
raw?: { reactMessage?: string };
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Cause categories
|
|
207
|
+
|
|
208
|
+
The classifier runs an ordered list of rules; the first match above a confidence
|
|
209
|
+
threshold wins, otherwise `unknown`. Custom `classify` rules run first.
|
|
210
|
+
|
|
211
|
+
> **What is the "Learn more →" link?** Every report (overlay + console) carries a
|
|
212
|
+
> `docsUrl` that deep-links to the matching section **below** (e.g. the locale
|
|
213
|
+
> report links to [`#cause-locale-format`](#cause-locale-format)). Each section
|
|
214
|
+
> also cites the authoritative React/MDN reference for that cause.
|
|
215
|
+
|
|
216
|
+
<a id="cause-non-deterministic-value"></a>
|
|
217
|
+
|
|
218
|
+
### non-deterministic-value
|
|
219
|
+
|
|
220
|
+
<img src="docs/screenshots/cause-non-deterministic-value.png" alt="non-deterministic-value report" width="420">
|
|
221
|
+
|
|
222
|
+
Server and client rendered different random-looking values (UUID, token, React
|
|
223
|
+
`:r…:` id, or `Math.random()` output).
|
|
224
|
+
**Fix:** `useId()` for ids; generate randomness after mount; never call
|
|
225
|
+
`Math.random()`/`crypto` in render.
|
|
226
|
+
**Reference:** [React `useId()`](https://react.dev/reference/react/useId).
|
|
227
|
+
|
|
228
|
+
<a id="cause-date-time"></a>
|
|
229
|
+
|
|
230
|
+
### date-time
|
|
231
|
+
|
|
232
|
+
<img src="docs/screenshots/cause-date-time.png" alt="date-time report" width="420">
|
|
233
|
+
|
|
234
|
+
Values are dates/times that differ by a small delta — the clock or timezone moved
|
|
235
|
+
between server and client render.
|
|
236
|
+
**Fix:** render time after mount, or pass one server timestamp down and pin the
|
|
237
|
+
timezone when formatting.
|
|
238
|
+
**Reference:** [React — different client/server content](https://react.dev/reference/react-dom/client/hydrateRoot#handling-different-client-and-server-content).
|
|
239
|
+
|
|
240
|
+
<a id="cause-locale-format"></a>
|
|
241
|
+
|
|
242
|
+
### locale-format
|
|
243
|
+
|
|
244
|
+
<img src="docs/screenshots/cause-locale-format.png" alt="locale-format report" width="420">
|
|
245
|
+
|
|
246
|
+
Same underlying value, different formatting: **Arabic-Indic ٠١٢ vs Latin 012**,
|
|
247
|
+
decimal/thousand separators (`1,234.56` vs `1.234,56`), or date order (MM/DD vs
|
|
248
|
+
DD/MM).
|
|
249
|
+
**Fix:** pass an explicit `locale` + timezone to `Intl` on both sides, or format
|
|
250
|
+
after mount.
|
|
251
|
+
**Reference:** [MDN `Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat).
|
|
252
|
+
|
|
253
|
+
<a id="cause-browser-only-api"></a>
|
|
254
|
+
|
|
255
|
+
### browser-only-api
|
|
256
|
+
|
|
257
|
+
<img src="docs/screenshots/cause-browser-only-api.png" alt="browser-only-api report" width="420">
|
|
258
|
+
|
|
259
|
+
The client rendered content the server left empty — a read of `window`,
|
|
260
|
+
`document`, `localStorage`, `navigator`, or `matchMedia` during render.
|
|
261
|
+
**Fix:** gate behind a mounted flag / `useEffect`, or use `useSyncExternalStore`
|
|
262
|
+
with a server snapshot.
|
|
263
|
+
**Reference:** [React `useSyncExternalStore` (server rendering)](https://react.dev/reference/react/useSyncExternalStore#adding-support-for-server-rendering).
|
|
264
|
+
|
|
265
|
+
<a id="cause-viewport-branching"></a>
|
|
266
|
+
|
|
267
|
+
### viewport-branching
|
|
268
|
+
|
|
269
|
+
A whole subtree was added/removed/swapped — usually a JS width check that
|
|
270
|
+
branches the tree at first render.
|
|
271
|
+
**Fix:** switch with CSS media queries at first paint, not JavaScript.
|
|
272
|
+
**Reference:** [MDN — CSS media queries](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries).
|
|
273
|
+
|
|
274
|
+
<a id="cause-invalid-html-nesting"></a>
|
|
275
|
+
|
|
276
|
+
### invalid-html-nesting
|
|
277
|
+
|
|
278
|
+
<img src="docs/screenshots/cause-invalid-html-nesting.png" alt="invalid-html-nesting report" width="420">
|
|
279
|
+
|
|
280
|
+
A node was moved/ejected because the markup is invalid (`<div>` in `<p>`, nested
|
|
281
|
+
`<a>`). The browser repairs the server DOM so it no longer matches React.
|
|
282
|
+
**Fix:** correct the markup validity.
|
|
283
|
+
**Reference:** [MDN — `<p>` (permitted content)](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/p).
|
|
284
|
+
|
|
285
|
+
<a id="cause-whitespace-minification"></a>
|
|
286
|
+
|
|
287
|
+
### whitespace-minification
|
|
288
|
+
|
|
289
|
+
The mismatch is whitespace-only — an HTML minifier collapsed whitespace around
|
|
290
|
+
the root differently from React.
|
|
291
|
+
**Fix:** check your minifier settings (e.g. `conservativeCollapse`) around the
|
|
292
|
+
app root.
|
|
293
|
+
**Reference:** [React — text content hydration](https://react.dev/reference/react-dom/client/hydrateRoot#handling-different-client-and-server-content).
|
|
294
|
+
|
|
295
|
+
<a id="cause-third-party-dom-mutation"></a>
|
|
296
|
+
|
|
297
|
+
### third-party-dom-mutation
|
|
298
|
+
|
|
299
|
+
<img src="docs/screenshots/cause-third-party-dom-mutation.png" alt="third-party-dom-mutation report" width="420">
|
|
300
|
+
|
|
301
|
+
An attribute was injected by a browser extension (Grammarly, ColorZilla, …) or an
|
|
302
|
+
early third-party script before hydration.
|
|
303
|
+
**Fix:** usually harmless — add `suppressHydrationWarning` to the leaf, or defer
|
|
304
|
+
third-party init to post-hydration.
|
|
305
|
+
**Reference:** [React `suppressHydrationWarning`](https://react.dev/reference/react-dom/components/common#suppressing-unavoidable-hydration-mismatch-errors).
|
|
306
|
+
|
|
307
|
+
<a id="cause-unknown"></a>
|
|
308
|
+
|
|
309
|
+
### unknown
|
|
310
|
+
|
|
311
|
+
A mismatch was detected but didn't match a known signature. The report still
|
|
312
|
+
shows the exact server vs client values and node path so you can diagnose it.
|
|
313
|
+
**Fix:** compare the two values — the cause is usually one of the categories
|
|
314
|
+
above. If you find a reliable signal, add a custom rule via the `classify`
|
|
315
|
+
option (see [CONTRIBUTING.md](CONTRIBUTING.md)).
|
|
316
|
+
**Reference:** [React — hydration mismatch errors](https://react.dev/reference/react-dom/client/hydrateRoot#handling-different-client-and-server-content).
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## How it works
|
|
321
|
+
|
|
322
|
+
1. **Snapshot** — a tiny inline script captures each hydration root's server
|
|
323
|
+
`innerHTML` before hydration (at `document.readyState === 'interactive'`,
|
|
324
|
+
which runs before deferred scripts).
|
|
325
|
+
2. **Detect** — `onRecoverableError` where you own `hydrateRoot`; otherwise a
|
|
326
|
+
dev-only `console.error` interceptor catches React's hydration warnings (the
|
|
327
|
+
Next.js fallback).
|
|
328
|
+
3. **Diff** — once, right after hydration (and again on each detection signal),
|
|
329
|
+
walk the server snapshot and the live DOM in parallel and find the first
|
|
330
|
+
divergence + its selector path. Framework/extension noise (scripts, the
|
|
331
|
+
overlay itself) is skipped. There is **no standing observer**, so legitimate
|
|
332
|
+
post-hydration updates are never mistaken for mismatches.
|
|
333
|
+
4. **Classify** — ordered heuristics assign a category + confidence + fix. If the
|
|
334
|
+
DOM diff can't locate it (e.g. invalid nesting the browser silently
|
|
335
|
+
repaired), the tool falls back to React's message.
|
|
336
|
+
5. **Report** — overlay (plain DOM in a shadow root, never the app's React) +
|
|
337
|
+
grouped console block + `onReport` callback. Deduped, capped at `maxReports`.
|
|
338
|
+
|
|
339
|
+
Everything above is gated on `process.env.NODE_ENV !== 'production'` and
|
|
340
|
+
tree-shakes to a no-op in production. CI enforces it with a size budget
|
|
341
|
+
(`npm run size`).
|
|
342
|
+
|
|
343
|
+
## Is it safe? (production & privacy)
|
|
344
|
+
|
|
345
|
+
- **Zero production cost.** Every code path is gated on
|
|
346
|
+
`process.env.NODE_ENV !== 'production'`, written inline so bundlers fold it and
|
|
347
|
+
tree-shake the whole implementation. In a production build
|
|
348
|
+
`<HydrationInspector>` is a pass-through and `createHydrationInspector` returns
|
|
349
|
+
no-ops. CI's size budget fails if a real production bundle is larger than a
|
|
350
|
+
few bytes (measured with webpack + terser, what Next.js/CRA/Vite actually use).
|
|
351
|
+
- **Read-only.** The tool never modifies your app's DOM — it only appends its own
|
|
352
|
+
overlay container (isolated in a shadow root) and reads the server snapshot.
|
|
353
|
+
- **No app-React dependency for the overlay.** The overlay is plain DOM, so it
|
|
354
|
+
works even while your React tree is mid-recovery.
|
|
355
|
+
- **SSR-safe.** Detection is client-only; nothing runs (or patches `console`) on
|
|
356
|
+
the server.
|
|
357
|
+
- **No network, no telemetry, no dependencies.** Nothing leaves the browser.
|
|
358
|
+
`react`/`react-dom` are optional peers; there are **zero** runtime
|
|
359
|
+
dependencies. Reports go only to your overlay, your console, and your own
|
|
360
|
+
`onReport` callback.
|
|
361
|
+
- **Values are rendered as text** (never `innerHTML`), and docs links are
|
|
362
|
+
restricted to `http(s)` — no injection from mismatched content.
|
|
363
|
+
|
|
364
|
+
## FAQ / Troubleshooting
|
|
365
|
+
|
|
366
|
+
**Does it work outside Next.js?** Yes — anywhere React hydrates: Vite, CRA,
|
|
367
|
+
Remix, or your own SSR. Use `createHydrationInspector` where you own
|
|
368
|
+
`hydrateRoot`, or `<HydrationInspector>` + the snapshot script anywhere else. The
|
|
369
|
+
core engine (`why-hydration`) is framework-agnostic.
|
|
370
|
+
|
|
371
|
+
**Nothing shows up.** Confirm `NODE_ENV` isn't `production`, that
|
|
372
|
+
`<HydrationInspector>` wraps your app, and (for precise value diffs) that the
|
|
373
|
+
snapshot script is in `<head>` and runs before hydration. Without the snapshot
|
|
374
|
+
the tool still reports from React's console warnings.
|
|
375
|
+
|
|
376
|
+
**The "Learn more →" link 404s.** The links point to this README on GitHub
|
|
377
|
+
(`github.com/razan-aboushi/why-hydration#cause-…`). Publish the repo under that
|
|
378
|
+
name, or change `DOCS_BASE` in `src/core/classify/rules.ts` and the `repository`
|
|
379
|
+
field in `package.json` to match your URL.
|
|
380
|
+
|
|
381
|
+
**Can I send reports to my logging?** Yes — pass `onReport`; you receive the full
|
|
382
|
+
`HydrationReport`.
|
|
383
|
+
|
|
384
|
+
**Does it slow my app down?** No. It's dev-only, runs the diff once after
|
|
385
|
+
hydration (plus once per real React signal), and holds no standing observers.
|
|
386
|
+
|
|
387
|
+
## Contributing
|
|
388
|
+
|
|
389
|
+
Adding a cause category is a self-contained change — see
|
|
390
|
+
[CONTRIBUTING.md](CONTRIBUTING.md).
|
|
391
|
+
|
|
392
|
+
## License
|
|
393
|
+
|
|
394
|
+
[MIT](LICENSE) © [Razan Aboushi](https://github.com/razan-aboushi)
|