Skip to content

Commit 902ca32

Browse files
committed
Demo: add a hydration repro page for inline-style order and text separators
1 parent 3860aac commit 902ca32

5 files changed

Lines changed: 97 additions & 0 deletions

File tree

demo/client/StyleOrderHydrate.re

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
let element = Webapi.Dom.Document.querySelector("#root", Webapi.Dom.document);
2+
3+
switch (element) {
4+
| Some(el) =>
5+
let _ = ReactDOM.Client.hydrateRoot(el, <StyleOrderRepro />);
6+
();
7+
| None => Js.log("No root element found")
8+
};

demo/client/dune

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
(:script build.mjs)
3333
(:entrypoints
3434
"app/demo/client/HydrateRoot.re.js"
35+
"app/demo/client/StyleOrderHydrate.re.js"
3536
"app/demo/client/RenderRoot.re.js"
3637
"app/demo/client/SinglePageRSC.re.js"
3738
"app/demo/client/DummyRouterRSC.re.js"

demo/server/server.re

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ let server =
5050
),
5151
)
5252
),
53+
getAndPost(Routes.styleOrderHydration, _request =>
54+
Dream.html(
55+
ReactDOM.renderToString(
56+
<Document script="/static/demo/StyleOrderHydrate.re.js">
57+
<StyleOrderRepro />
58+
</Document>,
59+
),
60+
)
61+
),
5362
getAndPost(Routes.renderToStream, Pages.Comments.handler),
5463
getAndPost(Routes.singlePageRSC, Pages.SinglePageRSC.handler),
5564
getAndPost(Routes.dummyRouterRSC, Pages.DummyRouterRSC.handler),

demo/universal/native/shared/Routes.re

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ let singlePageRSC = "/demo/singlePageRSC";
77
let dummyRouterRSC = "/demo/dummyRouterRSC";
88
let dummyRouterRSCNoSSR = "/demo/dummyRouterRSC?ssr=false";
99
let router = "/demo/router";
10+
let styleOrderHydration = "/demo/styleOrderHydration";
1011

1112
let links = [|
1213
(
@@ -44,6 +45,11 @@ let links = [|
4445
"The same demo as dummyRouterRSC but without SSR. It SSR the shell of the page (head, body, etc), but not the app itself.",
4546
dummyRouterRSCNoSSR,
4647
),
48+
(
49+
"styleOrderHydration",
50+
"Minimal reproduction of a hydration mismatch (React #418) when inline-style CSS custom properties serialize in a different order on the server than on the client, as styled-ppx's CSS-extraction runtime surfaced (ahrefs/WEB-844). Must hydrate clean.",
51+
styleOrderHydration,
52+
),
4753
(
4854
"nestedRouterRSC",
4955
"A nested router with server components and SSR, client components and Suspense + React.use. It uses the same design as the dummyRouterRSC but with a more complex structure that can handle nested routes and dynamic segments.",
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/* Minimal reproduction of ahrefs/WEB-844: hydration mismatch (React error
2+
#418) caused by inline-style CSS custom properties serializing in a
3+
different order on the server (server-reason-react) than on the client
4+
(Melange/reason-react).
5+
6+
styled-ppx in CSS-extraction mode hoists every $(...) interpolation into an
7+
inline-style custom property and builds the style prop by folding
8+
ReactDOM.Style.unsafeAddProp over the vars, in declaration order (see
9+
styled-ppx packages/runtime/{native,melange}/CSS.ml). [makeStyle] below is
10+
that exact runtime, inlined.
11+
12+
reason-react's unsafeAddProp is Object.assign: a new key lands last, so the
13+
client keeps declaration order. server-reason-react <= 20260616 snapshots
14+
prepended in unsafeAddProp, so SSR emitted the vars reversed and React
15+
reported a hydration mismatch on every element carrying two or more hoisted
16+
vars. Fixed by c9daf826 ("Emit style properties in signature order,
17+
matching React", first released in 0.5.0). */
18+
19+
let makeStyle = vars =>
20+
List.fold_left(
21+
(style, (key, value)) =>
22+
ReactDOM.Style.unsafeAddProp(style, key, value),
23+
ReactDOM.Style.make(),
24+
vars,
25+
);
26+
27+
/* Mirrors the extracted CSS of the failing ahrefs.com `Link_Css.link`: a
28+
top-level `color: $(color)` plus a nested
29+
`@media (hover: hover) { &:hover { color: $(hoverColor) } }`. */
30+
let extractedCss = {|
31+
.repro-link { cursor: pointer; color: var(--color-czybvw); }
32+
@media (hover: hover) {
33+
.repro-link:hover { color: var(--hoverColor-1eveqlc); }
34+
}
35+
|};
36+
37+
[@react.component]
38+
let make = () => {
39+
let style =
40+
makeStyle([
41+
("--color-czybvw", "#3A57FC"),
42+
("--hoverColor-1eveqlc", "#F75A03"),
43+
]);
44+
<DemoLayout background=Theme.Color.Gray2>
45+
<style> {React.string(extractedCss)} </style>
46+
<Stack gap=8 justify=`start>
47+
<h1 className="text-xl font-bold">
48+
{React.string("Hydration: inline-style custom property order")}
49+
</h1>
50+
<p className="text-sm text-gray-500">
51+
{React.string(
52+
"This anchor carries two hoisted custom properties, built exactly "
53+
++ "like styled-ppx's CSS-extraction runtime does (a fold over "
54+
++ "ReactDOM.Style.unsafeAddProp). If the server serializes them "
55+
++ "in a different order than the client, hydration fails with "
56+
++ "React error #418. Open the console: it must be free of "
57+
++ "hydration errors, and hovering the link must turn it orange.",
58+
)}
59+
</p>
60+
<a className="repro-link text-l font-bold" href="#" style>
61+
{React.string("I must be blue, orange on hover, and hydrate clean")}
62+
</a>
63+
/* Adjacent text nodes under a static-optimizable element: SSR must
64+
emit a <!-- --> separator between them or hydration fails with
65+
React #418 (the ahrefs.com LandingHero H1 shape, WEB-844). */
66+
<p className="text-sm">
67+
{React.string("Adjacent")}
68+
{React.string(" text nodes must hydrate clean too ")}
69+
<i> {React.string("(text separator regression)")} </i>
70+
</p>
71+
</Stack>
72+
</DemoLayout>;
73+
};

0 commit comments

Comments
 (0)