Skip to content

Commit aa12b3c

Browse files
Fix useId mismatch due to top level Fragments
1 parent 90d92e6 commit aa12b3c

File tree

5 files changed

+80
-26
lines changed

5 files changed

+80
-26
lines changed

.changeset/few-elephants-occur.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'preact-render-to-string': patch
3+
---
4+
5+
Fix vnode masks not matching with core due to top level component Fragments

package-lock.json

Lines changed: 14 additions & 24 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@
115115
"lint-staged": "^10.5.3",
116116
"microbundle": "^0.13.0",
117117
"mocha": "^8.2.1",
118-
"preact": "^10.5.7",
118+
"preact": "^10.11.1",
119119
"prettier": "^2.2.1",
120120
"sinon": "^9.2.2",
121121
"sinon-chai": "^3.5.0",

src/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,12 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
239239
}
240240
}
241241

242+
// When a component returns a Fragment node we flatten it in core, so we
243+
// need to mirror that logic here too
244+
let isTopLevelFragment =
245+
rendered != null && rendered.type === Fragment && rendered.key == null;
246+
rendered = isTopLevelFragment ? rendered.props.children : rendered;
247+
242248
// Recurse into children before invoking the after-diff hook
243249
const str = _renderToString(
244250
rendered,

test/render.test.js

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
useContext,
66
useEffect,
77
useLayoutEffect,
8-
useMemo
8+
useMemo,
9+
useId
910
} from 'preact/hooks';
1011
import { expect } from 'chai';
1112
import { spy, stub, match } from 'sinon';
@@ -1268,4 +1269,56 @@ describe('render', () => {
12681269
it('should not render function children', () => {
12691270
expect(render(<div>{() => {}}</div>)).to.equal('<div></div>');
12701271
});
1272+
1273+
describe('vnode masks (useId)', () => {
1274+
it('should work with Fragments', () => {
1275+
const ids = [];
1276+
function Foo() {
1277+
const id = useId();
1278+
ids.push(id);
1279+
return <p>{id}</p>;
1280+
}
1281+
1282+
function Bar(props) {
1283+
return props.children;
1284+
}
1285+
1286+
function App() {
1287+
return (
1288+
<Bar>
1289+
<Foo />
1290+
<Fragment>
1291+
<Foo />
1292+
</Fragment>
1293+
</Bar>
1294+
);
1295+
}
1296+
1297+
render(<App />);
1298+
expect(ids[0]).not.to.equal(ids[1]);
1299+
});
1300+
1301+
it('should skip component top level Fragment child', () => {
1302+
const Wrapper = ({ children }) => <Fragment>{children}</Fragment>;
1303+
1304+
function Foo() {
1305+
const id = useId();
1306+
return <p>{id}</p>;
1307+
}
1308+
1309+
function App() {
1310+
const id = useId();
1311+
return (
1312+
<div>
1313+
<p>{id}</p>
1314+
<Wrapper>
1315+
<Foo />
1316+
</Wrapper>
1317+
</div>
1318+
);
1319+
}
1320+
1321+
expect(render(<App />)).to.equal('<div><p>P481</p><p>P476951</p></div>');
1322+
});
1323+
});
12711324
});

0 commit comments

Comments
 (0)