Skip to content

Commit f771f75

Browse files
kedarvartakematipicoPrincesseuh
authored
fix(react): Fix hydration mismatch for experimentalReactChildren (#15146)
* feat: Map HTML attributes to React equivalents and generate unique keys for parsed children. * Update packages/integrations/react/src/client.ts Co-authored-by: Emanuele Stoppa <my.burning@gmail.com> * Update packages/integrations/react/src/client.ts Co-authored-by: Emanuele Stoppa <my.burning@gmail.com> * Create nine-points-dress.md --------- Co-authored-by: Emanuele Stoppa <my.burning@gmail.com> Co-authored-by: Erika <3019731+Princesseuh@users.noreply.github.com>
1 parent ccecb8f commit f771f75

File tree

5 files changed

+64
-5
lines changed

5 files changed

+64
-5
lines changed

.changeset/nine-points-dress.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@astrojs/react': patch
3+
---
4+
5+
Fixes hydration mismatch when using `experimentalReactChildren`

packages/integrations/react/src/client.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,33 @@ function isAlreadyHydrated(element: HTMLElement) {
1010
}
1111
}
1212

13-
function createReactElementFromDOMElement(element: any): any {
13+
const reactPropsMap: Record<string, string> = {
14+
class: 'className',
15+
for: 'htmlFor',
16+
};
17+
18+
let clientIds = 0;
19+
20+
function createReactElementFromDOMElement(element: any, id?: number, key?: number): any {
21+
if (id === undefined) {
22+
clientIds += 1;
23+
id = clientIds;
24+
key = 0;
25+
}
26+
1427
let attrs: Record<string, string> = {};
1528
for (const attr of element.attributes) {
16-
attrs[attr.name] = attr.value;
29+
const propName = reactPropsMap[attr.name] || attr.name;
30+
attrs[propName] = attr.value;
1731
}
18-
// If the element has no children, we can create a simple React element
32+
33+
attrs.key = `${id}-${key}`;
34+
1935
if (element.firstChild === null) {
2036
return createElement(element.localName, attrs);
2137
}
2238

39+
let childKey = 0;
2340
return createElement(
2441
element.localName,
2542
attrs,
@@ -28,7 +45,8 @@ function createReactElementFromDOMElement(element: any): any {
2845
if (c.nodeType === Node.TEXT_NODE) {
2946
return c.data;
3047
} else if (c.nodeType === Node.ELEMENT_NODE) {
31-
return createReactElementFromDOMElement(c);
48+
childKey += 1;
49+
return createReactElementFromDOMElement(c, id, childKey);
3250
} else {
3351
return undefined;
3452
}

packages/integrations/react/test/fixtures/react-component/src/pages/children.astro

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,11 @@ import WithChildren from '../components/WithChildren';
1414
<WithChildren id="two" client:load>
1515
<div>child 1</div><div>child 2</div>
1616
</WithChildren>
17+
18+
<!-- Test that class is properly mapped to className -->
19+
<WithChildren id="three" client:load>
20+
<span class="title">Hello</span>
21+
<span class="subtitle">World</span>
22+
</WithChildren>
1723
</body>
1824
</html>

packages/integrations/react/test/parsed-react-children.test.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,25 @@ describe('experimental react children', () => {
1313
const [imgVNode] = divVNode.props.children;
1414
assert.deepEqual(imgVNode.props.children, undefined);
1515
});
16+
17+
it('maps class attribute to className', () => {
18+
const [spanVNode] = convert('<span class="title">Hello</span>');
19+
assert.equal(spanVNode.props.className, 'title');
20+
assert.equal(spanVNode.props.class, undefined);
21+
});
22+
23+
it('generates unique keys for children', () => {
24+
const children = convert('<span class="first">A</span><span class="second">B</span>');
25+
assert.equal(children.length, 2);
26+
assert.ok(children[0].key, 'First child should have a key');
27+
assert.ok(children[1].key, 'Second child should have a key');
28+
assert.notEqual(children[0].key, children[1].key, 'Children should have unique keys');
29+
});
30+
31+
it('preserves other attributes alongside className', () => {
32+
const [spanVNode] = convert('<span class="title" id="main" data-test="value">Hello</span>');
33+
assert.equal(spanVNode.props.className, 'title');
34+
assert.equal(spanVNode.props.id, 'main');
35+
assert.equal(spanVNode.props['data-test'], 'value');
36+
});
1637
});

packages/integrations/react/test/react-component.test.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,16 @@ describe('React Components', () => {
119119
it('Client children passes option to the client', async () => {
120120
const html = await fixture.readFile('/children/index.html');
121121
const $ = cheerioLoad(html);
122-
assert.equal($('[data-react-children]').length, 1);
122+
assert.equal($('[data-react-children]').length, 2);
123+
});
124+
125+
it('Children with class attributes are properly rendered', async () => {
126+
const html = await fixture.readFile('/children/index.html');
127+
const $ = cheerioLoad(html);
128+
assert.equal($('#three .title').length, 1);
129+
assert.equal($('#three .subtitle').length, 1);
130+
assert.equal($('#three .title').text(), 'Hello');
131+
assert.equal($('#three .subtitle').text(), 'World');
123132
});
124133
});
125134

0 commit comments

Comments
 (0)