why-hydration 0.1.4 → 0.2.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/CHANGELOG.md +108 -1
- package/README.md +220 -33
- package/dist/{chunk-WQLUD25W.js → chunk-3OAI2ZHY.js} +375 -118
- package/dist/chunk-3OAI2ZHY.js.map +1 -0
- package/dist/{chunk-OGQEPU7G.cjs → chunk-DXPTZB3Z.cjs} +44 -2
- package/dist/chunk-DXPTZB3Z.cjs.map +1 -0
- package/dist/{chunk-DSO337ME.cjs → chunk-KX6G7I4Y.cjs} +384 -118
- package/dist/chunk-KX6G7I4Y.cjs.map +1 -0
- package/dist/chunk-MK3TYHKM.js +966 -0
- package/dist/chunk-MK3TYHKM.js.map +1 -0
- package/dist/chunk-R7IWOK5M.cjs +989 -0
- package/dist/chunk-R7IWOK5M.cjs.map +1 -0
- package/dist/{chunk-FV3PEJQE.js → chunk-XIM33ZGB.js} +44 -2
- package/dist/chunk-XIM33ZGB.js.map +1 -0
- package/dist/index.cjs +32 -24
- package/dist/index.d.cts +43 -5
- package/dist/index.d.ts +43 -5
- package/dist/index.js +2 -2
- package/dist/next/index.cjs +5 -5
- package/dist/next/index.d.cts +1 -1
- package/dist/next/index.d.ts +1 -1
- package/dist/next/index.js +3 -3
- package/dist/next/script.cjs +2 -2
- package/dist/next/script.js +1 -1
- package/dist/react.cjs +5 -5
- package/dist/react.d.cts +23 -3
- package/dist/react.d.ts +23 -3
- package/dist/react.js +3 -3
- package/dist/{types-6wTih14e.d.cts → types-C14vcpBO.d.cts} +9 -0
- package/dist/{types-6wTih14e.d.ts → types-C14vcpBO.d.ts} +9 -0
- package/package.json +1 -1
- package/dist/chunk-3KULWJ7A.js +0 -569
- package/dist/chunk-3KULWJ7A.js.map +0 -1
- package/dist/chunk-5CBGYE7A.cjs +0 -592
- package/dist/chunk-5CBGYE7A.cjs.map +0 -1
- package/dist/chunk-DSO337ME.cjs.map +0 -1
- package/dist/chunk-FV3PEJQE.js.map +0 -1
- package/dist/chunk-OGQEPU7G.cjs.map +0 -1
- package/dist/chunk-WQLUD25W.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,112 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- The overlay now speaks Arabic, and renders right-to-left data correctly in any language.
|
|
8
|
+
|
|
9
|
+
**Arabic overlay**
|
|
10
|
+
|
|
11
|
+
- On a page whose `<html lang>` is Arabic, the whole panel is in Arabic and
|
|
12
|
+
laid out right-to-left — title, buttons, category names, explanations, fixes,
|
|
13
|
+
and the scroll hint, with correct Arabic plurals. Every other page, including
|
|
14
|
+
right-to-left pages in languages the overlay does not translate, gets the
|
|
15
|
+
English panel as before.
|
|
16
|
+
- New `overlay={{ locale: 'auto' | 'en' | 'ar' }}` option. `'auto'` is the
|
|
17
|
+
default and follows `<html lang>`.
|
|
18
|
+
- The console output and the `explanation`/`suggestion` strings in `onReport`
|
|
19
|
+
stay English and are byte-identical to 0.1.5. Every built-in cause now also
|
|
20
|
+
carries `messageId` and `params`, which is how the overlay finds the
|
|
21
|
+
translation.
|
|
22
|
+
|
|
23
|
+
**Right-to-left rendering fixes**
|
|
24
|
+
|
|
25
|
+
- Arabic values no longer have their sentence punctuation moved to the wrong
|
|
26
|
+
end. Each value cell takes its direction from its own content, in either
|
|
27
|
+
panel language.
|
|
28
|
+
- Arabic values are drawn in a proportional face instead of a monospace
|
|
29
|
+
fallback, which rendered them with their letters disconnected.
|
|
30
|
+
- Selectors, component names, file paths and attribute names are isolated
|
|
31
|
+
left-to-right, and quoted values inside explanations are isolated one by one,
|
|
32
|
+
so brackets cannot mirror and tokens cannot swap places.
|
|
33
|
+
- The panel declares its own `lang`. Previously an English panel on an Arabic
|
|
34
|
+
page inherited `lang="ar"`, so screen readers voiced it with an Arabic voice.
|
|
35
|
+
|
|
36
|
+
**Other fixes**
|
|
37
|
+
|
|
38
|
+
- Invisible bidi marks (LRM, RLM, isolates…) are drawn as labelled badges in
|
|
39
|
+
the value cells, so the bidi-mark case no longer shows two identical-looking
|
|
40
|
+
values. Value cells also preserve whitespace, so whitespace-only mismatches
|
|
41
|
+
are visible.
|
|
42
|
+
- Component attribution from a React component stack no longer names a DOM tag
|
|
43
|
+
such as `<p>`; it names the nearest component you wrote.
|
|
44
|
+
- React's bare "hydration failed" message no longer adds an empty card next to
|
|
45
|
+
a real mismatch. When it is the only signal it is still reported, with text
|
|
46
|
+
that says React did not name the node, instead of "inspect the values above".
|
|
47
|
+
- React 19 hydration errors no longer produce junk reports. React 19 prints a
|
|
48
|
+
bulleted list of possible causes above its diff tree, and each bullet was
|
|
49
|
+
read as a removed ("server") diff line — five bogus "Unknown" reports per
|
|
50
|
+
mismatch, with the real client value paired against the first bullet. Only
|
|
51
|
+
the tree after React's hydration-mismatch link is read as a diff now.
|
|
52
|
+
|
|
53
|
+
## 0.1.5
|
|
54
|
+
|
|
55
|
+
### Patch Changes
|
|
56
|
+
|
|
57
|
+
- 7b16e20: Fix the inspector lifecycle, bound detection cost, and close detection gaps.
|
|
58
|
+
|
|
59
|
+
**Lifecycle**
|
|
60
|
+
|
|
61
|
+
- `createHydrationInspector()` is no longer dead after a React Strict Mode
|
|
62
|
+
remount. Its `<Provider>` only ever stopped the controller, so Strict Mode's
|
|
63
|
+
setup → cleanup → setup left the documented Vite/CRA/Remix integration
|
|
64
|
+
silently doing nothing for the rest of the session.
|
|
65
|
+
- The dev check no longer requires a `process` global. Vite, Rollup and esbuild
|
|
66
|
+
substitute `process.env.NODE_ENV` without defining `process` itself, which
|
|
67
|
+
compiled the old `typeof process !== 'undefined'` guard down to `false` and
|
|
68
|
+
disabled the inspector entirely in those bundlers.
|
|
69
|
+
- Scheduling races an animation frame against a timer, so a page that is hidden
|
|
70
|
+
at load — where the browser suspends `requestAnimationFrame` outright — is
|
|
71
|
+
still inspected.
|
|
72
|
+
- The console capture hands `console.error` back correctly when the same
|
|
73
|
+
listener subscribes twice, and its message cap now drops past-cap messages
|
|
74
|
+
instead of recording-but-forwarding them (which broke dedup exactly when the
|
|
75
|
+
cap was meant to engage).
|
|
76
|
+
|
|
77
|
+
**Cost**
|
|
78
|
+
|
|
79
|
+
- A burst of React warnings arriving in one frame triggers one diff pass rather
|
|
80
|
+
than one per warning.
|
|
81
|
+
- Captured server markup is parsed once per root and reused across the settling
|
|
82
|
+
window instead of being re-parsed on every pass.
|
|
83
|
+
- Retained warning text is bounded, so an app erroring in a render loop cannot
|
|
84
|
+
grow the per-pass cost without limit.
|
|
85
|
+
|
|
86
|
+
**Detection**
|
|
87
|
+
|
|
88
|
+
- `date-time` no longer fires on ordinary values. `Date.parse` accepts almost
|
|
89
|
+
anything — `100` is the year 100, `server-0` is the year 2000 — so prices,
|
|
90
|
+
counts and ids were being diagnosed as a clock or timezone drift. A value must
|
|
91
|
+
now look like a date by shape.
|
|
92
|
+
- Arabic formatting mismatches are detected when both sides use the same digit
|
|
93
|
+
script: grouping/decimal separators, field order and times in Arabic-Indic and
|
|
94
|
+
Persian digits previously fell through to `unknown`.
|
|
95
|
+
- Values differing only by invisible bidirectional control marks (LRM/RLM/ALM,
|
|
96
|
+
isolates) are detected and named. Different ICU versions emit different marks
|
|
97
|
+
for the same `Intl` call, so Node and the browser routinely produce strings
|
|
98
|
+
that look identical and are not.
|
|
99
|
+
- Report values are no longer truncated mid surrogate pair.
|
|
100
|
+
|
|
101
|
+
**Overlay**
|
|
102
|
+
|
|
103
|
+
- A panel re-mounted after being dismissed no longer reports a stale count or
|
|
104
|
+
promise scrolling it cannot do.
|
|
105
|
+
|
|
106
|
+
Note for anyone consuming `report.cause.category` in an `onReport` sink: the
|
|
107
|
+
`date-time` and `locale-format` fixes change which category some mismatches
|
|
108
|
+
resolve to.
|
|
109
|
+
|
|
3
110
|
## 0.1.4
|
|
4
111
|
|
|
5
112
|
- **License: restored MIT.** Re-added `LICENSE` (MIT) and set
|
|
@@ -93,7 +200,7 @@ Router, React 18 & 19). **0.1.0 is broken in Next.js — use 0.1.1 or later.**
|
|
|
93
200
|
the browser-normalized live DOM (`#hex` → `rgb()`, spacing) no longer produce
|
|
94
201
|
a false `unknown` mismatch.
|
|
95
202
|
- **`typesVersions`** added so subpath types resolve under `moduleResolution:
|
|
96
|
-
|
|
203
|
+
"node"` (fixes `next build` type errors in apps using classic resolution).
|
|
97
204
|
|
|
98
205
|
Verified live: locale-format, non-deterministic-value, date-time, and
|
|
99
206
|
browser-only-api classify correctly in App Router and Pages Router; the tool is
|
package/README.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/why-hydration)
|
|
4
4
|
[](https://www.npmjs.com/package/why-hydration)
|
|
5
|
-
[](https://github.com/razan-aboushi/why-hydration/actions/workflows/ci.yml)
|
|
6
|
+
[](#production-behavior)
|
|
7
|
+
[](#install)
|
|
6
8
|
[](LICENSE)
|
|
7
9
|
|
|
8
10
|
📦 **npm:** https://www.npmjs.com/package/why-hydration · 🐙 **GitHub:** https://github.com/razan-aboushi/why-hydration · 💼 **Author:** [Razan Aboushi](https://www.linkedin.com/in/razan-aboushi/)
|
|
@@ -58,9 +60,10 @@ together.
|
|
|
58
60
|
incidental page noise.
|
|
59
61
|
- 🌐 **Arabic-first locale detection** — digit-script mismatches (٠١٢ vs 012)
|
|
60
62
|
are a first-class cause, not an afterthought.
|
|
61
|
-
- 🔤 **
|
|
62
|
-
|
|
63
|
-
English.
|
|
63
|
+
- 🔤 **Speaks Arabic, right-to-left** — on an Arabic page (`<html lang="ar">`)
|
|
64
|
+
the overlay is in Arabic and laid out right-to-left; everywhere else it is in
|
|
65
|
+
English. Page data renders in its own direction in both, and invisible bidi
|
|
66
|
+
marks are shown instead of hidden.
|
|
64
67
|
- 🫧 **Zero production cost** — every code path is gated behind
|
|
65
68
|
`process.env.NODE_ENV`, and CI fails the build if the production bundle for
|
|
66
69
|
any entry point isn't tree-shaken to a no-op.
|
|
@@ -288,6 +291,14 @@ React/Next.js's own internal markup markers.
|
|
|
288
291
|
misattribute every following sibling. This is a bounded check, not a
|
|
289
292
|
standing `MutationObserver`, so legitimate DOM changes from your app's own
|
|
290
293
|
state updates after this window are never mistaken for a hydration issue.
|
|
294
|
+
|
|
295
|
+
The passes are also bounded in cost. React logs its warnings in bursts, so
|
|
296
|
+
every signal that lands in the same frame is coalesced into **one** diff
|
|
297
|
+
rather than one diff each, and the captured server markup — re-materialising
|
|
298
|
+
it is a full HTML parse of your server render — is parsed **once per root**
|
|
299
|
+
and reused across every pass. Scheduling races an animation frame against a
|
|
300
|
+
50 ms timer, so a page that is hidden at load (where the browser suspends
|
|
301
|
+
`requestAnimationFrame` entirely) still gets inspected.
|
|
291
302
|
4. **Classify.** Each divergence is passed through an ordered list of rules
|
|
292
303
|
(see [Cause categories](#cause-categories)); the first rule whose
|
|
293
304
|
confidence clears the threshold wins, otherwise the mismatch is reported as
|
|
@@ -321,6 +332,28 @@ production):
|
|
|
321
332
|
|
|
322
333
|
A page with no mismatches renders **nothing** — no overlay, no console output.
|
|
323
334
|
|
|
335
|
+
### Controlling the overlay
|
|
336
|
+
|
|
337
|
+
| Action | Effect |
|
|
338
|
+
| ------ | ------ |
|
|
339
|
+
| **Dismiss** button, or **Esc** | Removes the panel for the rest of the page load. |
|
|
340
|
+
| **✕** on the hint bar | Closes just the "scroll to see all" hint; the panel stays. |
|
|
341
|
+
| `overlay={false}` | Never mounts it at all — `onReport` and the console output still work. |
|
|
342
|
+
| `overlay={{ position }}` | `bottom-right` (default), `bottom-left`, `top-right`, `top-left`. |
|
|
343
|
+
| `overlay={{ locale }}` | `'auto'` (default) follows `<html lang>`; `'en'` or `'ar'` pins the language. See [RTL and Arabic support](#rtl-and-arabic-support). |
|
|
344
|
+
|
|
345
|
+
The overlay is a *view* over the collected reports, not the collector itself:
|
|
346
|
+
dismissing it does not stop detection, and `onReport` keeps firing. If a
|
|
347
|
+
mismatch is found after you dismissed it — a late signal, or a second hydration
|
|
348
|
+
error — the panel returns showing that mismatch, starting from a clean count
|
|
349
|
+
rather than resuming a stale one.
|
|
350
|
+
|
|
351
|
+
It renders in an isolated Shadow DOM, is never part of your app's tree, and is
|
|
352
|
+
excluded from its own diff, so it can never be mistaken for a mismatch.
|
|
353
|
+
Mismatched values are rendered as **text**, and "Learn more" links are
|
|
354
|
+
restricted to `http(s)` URLs, so nothing in a mismatched value can inject markup
|
|
355
|
+
or script into the panel.
|
|
356
|
+
|
|
324
357
|
### In a real app
|
|
325
358
|
|
|
326
359
|
Captured from a production Next.js app: every mismatch on the page collected
|
|
@@ -337,21 +370,82 @@ there are more than fit:
|
|
|
337
370
|
|
|
338
371
|
## RTL and Arabic support
|
|
339
372
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
373
|
+
An Arabic app gets the same diagnosis quality as an English one. That covers
|
|
374
|
+
both how the overlay renders and what the engine can actually detect.
|
|
375
|
+
|
|
376
|
+
### The overlay
|
|
377
|
+
|
|
378
|
+
**The overlay speaks the page's language.** On a page whose `<html lang>` is
|
|
379
|
+
Arabic (`ar`, `ar-SA`, `ar-EG`, …) the whole panel is in Arabic and laid out
|
|
380
|
+
right-to-left: title, buttons, category names, the explanation, the fix, and
|
|
381
|
+
the scroll hint, with correct Arabic plurals. Every other page gets the English
|
|
382
|
+
panel — including right-to-left pages in languages the overlay does not
|
|
383
|
+
translate, such as Hebrew or Persian, where it stays left-to-right rather than
|
|
384
|
+
mirroring English text. Pin it either way with `overlay={{ locale: 'en' }}` or
|
|
385
|
+
`overlay={{ locale: 'ar' }}`.
|
|
386
|
+
|
|
387
|
+
What stays English: the console output, and `cause.explanation` /
|
|
388
|
+
`cause.suggestion` in the report `onReport` receives. Those are what people log,
|
|
389
|
+
grep and paste into issues, so they do not change with the page. Each report
|
|
390
|
+
also carries `cause.messageId` and `cause.params`, which is how the overlay
|
|
391
|
+
finds the translation — and how your own tooling can, too. The "Learn more"
|
|
392
|
+
links point at this README, which is in English, and say so.
|
|
393
|
+
|
|
394
|
+
**Page data renders in its own direction, in either panel.** A mismatched value
|
|
395
|
+
can be in any script, so each value cell takes its direction from its own first
|
|
396
|
+
strong character. `السعر: ١٬٤٠٠ د.ك.` reads right-to-left with its full stop at
|
|
397
|
+
the end even inside the English panel, and `Price: 1,400 KWD.` reads
|
|
398
|
+
left-to-right inside the Arabic one. Selectors, component names, file paths and
|
|
399
|
+
attribute names are code, so they are always isolated left-to-right — otherwise
|
|
400
|
+
a right-to-left line would mirror their brackets and reorder their segments.
|
|
401
|
+
Class tokens and other values quoted inside a sentence are isolated one by one,
|
|
402
|
+
so two Arabic tokens cannot swap places. Arabic is drawn in a proportional face
|
|
403
|
+
(a monospace fallback renders it with its letters disconnected), and Arabic
|
|
404
|
+
labels are never letter-spaced or uppercased, which breaks their joining.
|
|
405
|
+
|
|
406
|
+
**What differs is what you see.** Value cells preserve whitespace, so a
|
|
407
|
+
whitespace-only mismatch no longer looks like two identical strings. Invisible
|
|
408
|
+
characters that change layout — `LRM`, `RLM`, `ALM`, the bidi embeddings and
|
|
409
|
+
isolates, zero-width space, BOM — are drawn as small labelled badges rather than
|
|
410
|
+
applied, so the [bidi-mark case](#cause-locale-format) shows a visible
|
|
411
|
+
difference instead of two cells that look the same.
|
|
412
|
+
|
|
413
|
+
**It is isolated from the page.** The panel lives in a Shadow DOM, and its
|
|
414
|
+
direction is set inside it, so nothing in the host page's CSS can flip it — the
|
|
415
|
+
`all` shorthand deliberately excludes `direction` (per spec), so
|
|
416
|
+
`:host { all: initial }` alone would not be enough. The panel also declares its
|
|
417
|
+
own `lang`, so a screen reader voices it in its own language: before this, an
|
|
418
|
+
English panel on an Arabic page was read aloud with an Arabic voice.
|
|
419
|
+
|
|
420
|
+
### Detection
|
|
421
|
+
|
|
422
|
+
Detection is direction-agnostic: every rule matches on the *shape* of a value,
|
|
423
|
+
not its script. Three Arabic-specific cases are worth calling out, because the
|
|
424
|
+
first is the one most people expect and the other two are the ones that
|
|
425
|
+
actually bite:
|
|
426
|
+
|
|
427
|
+
- **Different digit scripts.** Arabic-Indic `٠١٢` on one side, Latin `012` on
|
|
428
|
+
the other — the classic symptom of `Intl`/`toLocaleString` resolving to a
|
|
429
|
+
different locale on the server than in the browser. Reported as
|
|
430
|
+
[`locale-format`](#cause-locale-format) at 92% confidence.
|
|
431
|
+
- **Same digit script, different formatting.** An Arabic-first app renders
|
|
432
|
+
Arabic-Indic digits on *both* sides, so there is no script difference to key
|
|
433
|
+
off — only a grouping separator (`١٬٤٠٠` vs `١٤٠٠`), a decimal separator
|
|
434
|
+
(`١٢٣٤٫٥٦` vs `١٢٣٤.٥٦`), a field order, or a time. These are folded to Latin
|
|
435
|
+
before the numeric/date shape tests run, so they are classified exactly like
|
|
436
|
+
their English equivalents instead of falling through to `unknown`. Persian /
|
|
437
|
+
Extended Arabic-Indic digits (`۰۱۲`) are handled the same way.
|
|
438
|
+
- **Invisible bidi marks.** `Intl` wraps numbers and date fields in
|
|
439
|
+
bidirectional control characters (LRM, RLM, ALM, isolates) in RTL locales,
|
|
440
|
+
and *which* ones it emits differs between ICU versions — so Node and the
|
|
441
|
+
browser routinely format the same date into strings that are visually
|
|
442
|
+
identical and byte-different. This is the hardest hydration mismatch to debug
|
|
443
|
+
by eye, since the console diff looks like the same text twice. It is detected
|
|
444
|
+
and named explicitly.
|
|
445
|
+
|
|
446
|
+
Both directions are covered by the test suite as a matched pair
|
|
447
|
+
(`test/i18n.test.tsx`), plus overlay directionality in `test/direction.test.ts`,
|
|
448
|
+
so English and Arabic behaviour cannot drift apart.
|
|
355
449
|
|
|
356
450
|
---
|
|
357
451
|
|
|
@@ -414,11 +508,19 @@ interface HydrationInspectorHandle {
|
|
|
414
508
|
}
|
|
415
509
|
```
|
|
416
510
|
|
|
511
|
+
`Provider` owns the inspector's lifetime: it must actually be mounted, and
|
|
512
|
+
unmounting it tears the inspector down (overlay removed, `console.error`
|
|
513
|
+
handed back untouched). Mounting it again restarts detection cleanly rather
|
|
514
|
+
than stacking a second overlay or a second console patch — which is what makes
|
|
515
|
+
it safe under `<React.StrictMode>`, where React deliberately runs every mount
|
|
516
|
+
effect setup → cleanup → setup.
|
|
517
|
+
|
|
417
518
|
### `OverlayOptions`
|
|
418
519
|
|
|
419
520
|
```ts
|
|
420
521
|
interface OverlayOptions {
|
|
421
522
|
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'; // default 'bottom-right'
|
|
523
|
+
locale?: 'auto' | 'en' | 'ar'; // default 'auto' — follows <html lang>
|
|
422
524
|
}
|
|
423
525
|
```
|
|
424
526
|
|
|
@@ -467,9 +569,11 @@ interface HydrationReport {
|
|
|
467
569
|
cause: {
|
|
468
570
|
category: HydrationCauseCategory; // one of the categories below, or "unknown"
|
|
469
571
|
confidence: number; // 0–1
|
|
470
|
-
explanation: string;
|
|
471
|
-
suggestion: string;
|
|
572
|
+
explanation: string; // always English
|
|
573
|
+
suggestion: string; // always English
|
|
472
574
|
docsUrl?: string;
|
|
575
|
+
messageId?: string; // which message produced the text, e.g. "attribute-mismatch.class"
|
|
576
|
+
params?: Record<string, string | string[]>; // the values in it, e.g. { added: ['wide'], removed: [] }
|
|
473
577
|
};
|
|
474
578
|
raw?: { reactMessage?: string }; // the original React console message, if any
|
|
475
579
|
}
|
|
@@ -508,6 +612,14 @@ Server and client rendered different random-looking values (UUID, token, React
|
|
|
508
612
|
|
|
509
613
|
Values are dates/times that differ by a small delta — the clock or timezone
|
|
510
614
|
moved between server and client render.
|
|
615
|
+
|
|
616
|
+
Matched on **shape**, not on whether `Date.parse` happens to accept the value:
|
|
617
|
+
ISO dates, `D/M/YYYY`-style dates, clock times, month names with a number, and
|
|
618
|
+
10–13 digit epoch timestamps. That distinction matters because `Date.parse` is
|
|
619
|
+
extremely permissive — it reads `100` as the year 100 and `server-0` as the
|
|
620
|
+
year 2000 — so an ordinary price, count, or id would otherwise be diagnosed as
|
|
621
|
+
a clock drift.
|
|
622
|
+
|
|
511
623
|
**Fix:** render time after mount, or pass one server timestamp down and pin
|
|
512
624
|
the timezone when formatting.
|
|
513
625
|
**Reference:** [React — different client/server content](https://react.dev/reference/react-dom/client/hydrateRoot#handling-different-client-and-server-content).
|
|
@@ -518,11 +630,21 @@ the timezone when formatting.
|
|
|
518
630
|
|
|
519
631
|
<img src="docs/screenshots/cause-locale-format.png" alt="locale-format report" width="420">
|
|
520
632
|
|
|
521
|
-
Same underlying value, different formatting:
|
|
522
|
-
|
|
523
|
-
|
|
633
|
+
Same underlying value, different formatting. Covers:
|
|
634
|
+
|
|
635
|
+
- **Different digit scripts** — Arabic-Indic `٠١٢` vs Latin `012`, or Persian
|
|
636
|
+
`۰۱۲` vs Latin.
|
|
637
|
+
- **Different separators or field order** — `1,234.56` vs `1.234,56`,
|
|
638
|
+
`١٬٤٠٠` vs `١٤٠٠`, MM/DD vs DD/MM. Detected in Arabic-Indic and Persian
|
|
639
|
+
digits as well as Latin, so an app that renders the same digit script on both
|
|
640
|
+
sides is still diagnosed.
|
|
641
|
+
- **Invisible bidirectional marks** — values that are identical on screen but
|
|
642
|
+
differ by LRM/RLM/ALM or isolate characters, which `Intl` adds around numbers
|
|
643
|
+
and dates in RTL locales and which different ICU versions (Node vs the
|
|
644
|
+
browser) emit differently.
|
|
645
|
+
|
|
524
646
|
**Fix:** pass an explicit `locale` and timezone to `Intl` on both sides, or
|
|
525
|
-
format after mount.
|
|
647
|
+
format after mount. See also [RTL and Arabic support](#rtl-and-arabic-support).
|
|
526
648
|
**Reference:** [MDN `Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat).
|
|
527
649
|
|
|
528
650
|
<a id="cause-third-party-dom-mutation"></a>
|
|
@@ -612,6 +734,13 @@ drive the visual change with a CSS media query instead of a JS class toggle.
|
|
|
612
734
|
A mismatch was detected but didn't match any of the rules above. The report
|
|
613
735
|
still shows the exact server vs. client values and node path so you can
|
|
614
736
|
diagnose it directly.
|
|
737
|
+
|
|
738
|
+
React also logs a bare "hydration failed" message that names no node at all.
|
|
739
|
+
When the DOM diff or another React message has already located the mismatch,
|
|
740
|
+
that message adds nothing and is not shown. When it is the *only* signal —
|
|
741
|
+
typically because the snapshot script is missing — it is reported once, with an
|
|
742
|
+
explanation that says React did not name the node and a fix that points at the
|
|
743
|
+
snapshot script, rather than asking you to inspect values that do not exist.
|
|
615
744
|
**Fix:** compare the two values — the cause is usually one of the categories
|
|
616
745
|
above. If you find a reliable signal for it, add a custom rule via the
|
|
617
746
|
`classify` option (see [Contributing](#contributing)).
|
|
@@ -628,11 +757,24 @@ above. If you find a reliable signal for it, add a custom rule via the
|
|
|
628
757
|
check, written so bundlers can statically fold it away. In a production
|
|
629
758
|
build, `<HydrationInspector>` becomes a plain pass-through and
|
|
630
759
|
`createHydrationInspector()` returns no-op functions.
|
|
760
|
+
- Production is **opt-out**, not opt-in: the internal dev check treats an
|
|
761
|
+
explicit `NODE_ENV === 'production'` as production and everything else,
|
|
762
|
+
including a bundle where `process` was never defined at all, as development.
|
|
763
|
+
That matters because Vite, Rollup and esbuild substitute
|
|
764
|
+
`process.env.NODE_ENV` without shimming `process` itself — requiring
|
|
765
|
+
`process` to exist would silently disable the inspector for all of them. It
|
|
766
|
+
cannot leak dev code into a production bundle, because the entry-point gates
|
|
767
|
+
above have already been folded away by then.
|
|
631
768
|
- This is enforced, not just claimed: the project's CI pipeline runs a size
|
|
632
769
|
budget (`npm run size`) against a real production bundle built with webpack
|
|
633
770
|
and terser — the same toolchain Next.js and CRA use for production — and
|
|
634
771
|
fails the build if the tree-shaken output for any entry point isn't reduced
|
|
635
|
-
to a near-empty stub.
|
|
772
|
+
to a near-empty stub. The budgets sit just above today's output, so even a
|
|
773
|
+
small leak fails CI. Verified independently against Rollup, which is what
|
|
774
|
+
Vite uses for production builds: **99 B** minified + gzipped under webpack,
|
|
775
|
+
and **156 B** minified (not gzipped) under Rollup — versus ~59 KB minified
|
|
776
|
+
for the same entry built for development. The Arabic translations exist only
|
|
777
|
+
in that development build.
|
|
636
778
|
- `<HydrationSnapshotScript>` also renders `null` outside development, so no
|
|
637
779
|
snapshot script is emitted into your production HTML.
|
|
638
780
|
|
|
@@ -662,6 +804,16 @@ above. If you find a reliable signal for it, add a custom rule via the
|
|
|
662
804
|
- **Bounded, not continuous.** Detection re-checks the DOM a handful of times
|
|
663
805
|
in the ~1.5 seconds after hydration and then stops — there is no standing
|
|
664
806
|
`MutationObserver` watching your app for the rest of its lifetime.
|
|
807
|
+
- **Bounded in cost, too.** A burst of React warnings landing in one frame
|
|
808
|
+
triggers one diff pass, not one per warning. The captured server markup is
|
|
809
|
+
parsed once per root and reused across passes rather than re-parsed each
|
|
810
|
+
time. Retained warning text is capped, so an app erroring in a render loop
|
|
811
|
+
cannot grow the per-pass cost without limit, and reports are capped by
|
|
812
|
+
`maxReports` (default 25).
|
|
813
|
+
- **Symmetrical teardown.** Unmounting the inspector restores `console.error`
|
|
814
|
+
to exactly the function it replaced, removes the overlay, and clears every
|
|
815
|
+
pending timer — so a remount (including React Strict Mode's deliberate
|
|
816
|
+
double-mount in dev) leaves one overlay and one console patch, not two.
|
|
665
817
|
|
|
666
818
|
---
|
|
667
819
|
|
|
@@ -673,11 +825,30 @@ Remix, or a custom SSR setup. Use `createHydrationInspector` where you own
|
|
|
673
825
|
The core engine (`why-hydration`) is framework-agnostic.
|
|
674
826
|
|
|
675
827
|
**Nothing shows up, but I know there's a mismatch.** Confirm `NODE_ENV` isn't
|
|
676
|
-
`production`, that `<HydrationInspector>` (or its `Provider`) actually
|
|
677
|
-
the part of the tree that mismatches, and that the
|
|
678
|
-
in `<head>` and runs before your app's hydration
|
|
679
|
-
script, the tool still reports mismatches it can
|
|
680
|
-
console warning, but loses the precise DOM-level diff.
|
|
828
|
+
`production`, that `<HydrationInspector>` (or its `Provider`) is actually
|
|
829
|
+
**mounted** and wraps the part of the tree that mismatches, and that the
|
|
830
|
+
snapshot script is present in `<head>` and runs before your app's hydration
|
|
831
|
+
script. Without the snapshot script, the tool still reports mismatches it can
|
|
832
|
+
parse from React's own console warning, but loses the precise DOM-level diff.
|
|
833
|
+
If you passed `roots`, check the console for a
|
|
834
|
+
`[why-hydration] Skipping root:` warning — a root with no captured server HTML
|
|
835
|
+
can never produce a DOM-level report, and it says so rather than failing
|
|
836
|
+
silently.
|
|
837
|
+
|
|
838
|
+
**Does it work under `<React.StrictMode>`?** Yes. Strict Mode runs every mount
|
|
839
|
+
effect setup → cleanup → setup in development, which tears the inspector down
|
|
840
|
+
and rebuilds it. You get one overlay, one console patch, and each mismatch
|
|
841
|
+
reported once — the warnings React logged during the first pass are replayed
|
|
842
|
+
to the rebuilt inspector rather than lost.
|
|
843
|
+
|
|
844
|
+
**I loaded the page in a background tab and got nothing.** Fixed — browsers
|
|
845
|
+
suspend `requestAnimationFrame` on hidden pages, so scheduling races a frame
|
|
846
|
+
against a 50 ms timer and no longer depends on the page being painted.
|
|
847
|
+
|
|
848
|
+
**The component name in a report was a tag like `<p>`.** Fixed. React's
|
|
849
|
+
component stacks list host elements first (`at p`, `at span`), and those were
|
|
850
|
+
being taken as the component. The nearest component you wrote is used instead,
|
|
851
|
+
and when there is none the report names no component rather than a tag.
|
|
681
852
|
|
|
682
853
|
**The "Learn more →" link 404s.** The links point at this README on GitHub
|
|
683
854
|
(`github.com/razan-aboushi/why-hydration#cause-…`). If you've forked the
|
|
@@ -700,8 +871,24 @@ production. It's dev-only, runs a bounded diff across roughly 1.5 seconds
|
|
|
700
871
|
right after hydration, then stops.
|
|
701
872
|
|
|
702
873
|
**Does it work with `<html dir="rtl">`?** Yes — see
|
|
703
|
-
[RTL and Arabic support](#rtl-and-arabic-support). The
|
|
704
|
-
|
|
874
|
+
[RTL and Arabic support](#rtl-and-arabic-support). The panel's language follows
|
|
875
|
+
`<html lang>`, not `dir`: an Arabic page gets the Arabic right-to-left panel,
|
|
876
|
+
and a right-to-left page in another language gets the English left-to-right
|
|
877
|
+
one. Detection covers Arabic-script formatting mismatches on both sides, not
|
|
878
|
+
just Arabic-vs-Latin.
|
|
879
|
+
|
|
880
|
+
**I want the English panel on my Arabic site (or the reverse).** Pass
|
|
881
|
+
`overlay={{ locale: 'en' }}` (or `'ar'`). The console and `onReport` are English
|
|
882
|
+
either way.
|
|
883
|
+
|
|
884
|
+
**My Arabic app reports a mismatch between two values that look the same.** They
|
|
885
|
+
differ by invisible bidirectional control characters — `Intl` adds LRM/RLM/isolate
|
|
886
|
+
marks around numbers and dates in RTL locales, and Node's ICU and the browser's
|
|
887
|
+
ICU do not always agree on which. The report names this explicitly under
|
|
888
|
+
[`locale-format`](#cause-locale-format), and the overlay draws each invisible
|
|
889
|
+
mark as a small labelled badge (`RLM`, `LRM`, …) in the value cell, so you can
|
|
890
|
+
see which side has it. Format the value in one place and pass the string down,
|
|
891
|
+
or add `suppressHydrationWarning` if the marks are harmless.
|
|
705
892
|
|
|
706
893
|
---
|
|
707
894
|
|