Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/nine-points-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/react': patch
---

Fixes hydration mismatch when using `experimentalReactChildren`
26 changes: 22 additions & 4 deletions packages/integrations/react/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,33 @@ function isAlreadyHydrated(element: HTMLElement) {
}
}

function createReactElementFromDOMElement(element: any): any {
const reactPropsMap: Record<string, string> = {
class: 'className',
for: 'htmlFor',
};

let clientIds = 0;

function createReactElementFromDOMElement(element: any, id?: number, key?: number): any {
if (id === undefined) {
clientIds += 1;
id = clientIds;
key = 0;
}

let attrs: Record<string, string> = {};
for (const attr of element.attributes) {
attrs[attr.name] = attr.value;
const propName = reactPropsMap[attr.name] || attr.name;
attrs[propName] = attr.value;
}
// If the element has no children, we can create a simple React element

attrs.key = `${id}-${key}`;

if (element.firstChild === null) {
return createElement(element.localName, attrs);
}

let childKey = 0;
return createElement(
element.localName,
attrs,
Expand All @@ -28,7 +45,8 @@ function createReactElementFromDOMElement(element: any): any {
if (c.nodeType === Node.TEXT_NODE) {
return c.data;
} else if (c.nodeType === Node.ELEMENT_NODE) {
return createReactElementFromDOMElement(c);
childKey += 1;
return createReactElementFromDOMElement(c, id, childKey);
} else {
return undefined;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,11 @@ import WithChildren from '../components/WithChildren';
<WithChildren id="two" client:load>
<div>child 1</div><div>child 2</div>
</WithChildren>

<!-- Test that class is properly mapped to className -->
<WithChildren id="three" client:load>
<span class="title">Hello</span>
<span class="subtitle">World</span>
</WithChildren>
</body>
</html>
21 changes: 21 additions & 0 deletions packages/integrations/react/test/parsed-react-children.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,25 @@ describe('experimental react children', () => {
const [imgVNode] = divVNode.props.children;
assert.deepEqual(imgVNode.props.children, undefined);
});

it('maps class attribute to className', () => {
const [spanVNode] = convert('<span class="title">Hello</span>');
assert.equal(spanVNode.props.className, 'title');
assert.equal(spanVNode.props.class, undefined);
});

it('generates unique keys for children', () => {
const children = convert('<span class="first">A</span><span class="second">B</span>');
assert.equal(children.length, 2);
assert.ok(children[0].key, 'First child should have a key');
assert.ok(children[1].key, 'Second child should have a key');
assert.notEqual(children[0].key, children[1].key, 'Children should have unique keys');
});

it('preserves other attributes alongside className', () => {
const [spanVNode] = convert('<span class="title" id="main" data-test="value">Hello</span>');
assert.equal(spanVNode.props.className, 'title');
assert.equal(spanVNode.props.id, 'main');
assert.equal(spanVNode.props['data-test'], 'value');
});
});
11 changes: 10 additions & 1 deletion packages/integrations/react/test/react-component.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,16 @@ describe('React Components', () => {
it('Client children passes option to the client', async () => {
const html = await fixture.readFile('/children/index.html');
const $ = cheerioLoad(html);
assert.equal($('[data-react-children]').length, 1);
assert.equal($('[data-react-children]').length, 2);
});

it('Children with class attributes are properly rendered', async () => {
const html = await fixture.readFile('/children/index.html');
const $ = cheerioLoad(html);
assert.equal($('#three .title').length, 1);
assert.equal($('#three .subtitle').length, 1);
assert.equal($('#three .title').text(), 'Hello');
assert.equal($('#three .subtitle').text(), 'World');
});
});

Expand Down
Loading