Skip to content

Commit d9ecbf1

Browse files
authored
Fix useId in JSX renderer (#439)
* Fix useId in JSX renderer * Add changeset
1 parent 534a37a commit d9ecbf1

File tree

3 files changed

+96
-8
lines changed

3 files changed

+96
-8
lines changed
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+
Ensure `useId()` produces unique IDs when using the JSX renderer

src/pretty.js

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,16 @@ import {
1616
isDirty,
1717
unsetDirty
1818
} from './lib/util.js';
19-
import { COMMIT, DIFF, DIFFED, RENDER, SKIP_EFFECTS } from './lib/constants.js';
20-
import { options, Fragment } from 'preact';
19+
import {
20+
COMMIT,
21+
DIFF,
22+
DIFFED,
23+
RENDER,
24+
SKIP_EFFECTS,
25+
PARENT,
26+
CHILDREN
27+
} from './lib/constants.js';
28+
import { options, Fragment, h } from 'preact';
2129

2230
// components without names, kept as a hash for later comparison to return consistent UnnamedComponentXX names.
2331
const UNNAMED = [];
@@ -47,8 +55,19 @@ export default function renderToStringPretty(vnode, context, opts, _inner) {
4755
const previousSkipEffects = options[SKIP_EFFECTS];
4856
options[SKIP_EFFECTS] = true;
4957

58+
const parent = h(Fragment, null);
59+
parent[CHILDREN] = [vnode];
60+
5061
try {
51-
return _renderToStringPretty(vnode, context || {}, opts, _inner);
62+
return _renderToStringPretty(
63+
vnode,
64+
context || {},
65+
opts,
66+
_inner,
67+
false,
68+
undefined,
69+
parent
70+
);
5271
} finally {
5372
// options._commit, we don't schedule any effects in this library right now,
5473
// so we can pass an empty queue to this hook.
@@ -64,7 +83,8 @@ function _renderToStringPretty(
6483
opts,
6584
inner,
6685
isSvgMode,
67-
selectValue
86+
selectValue,
87+
parent
6888
) {
6989
if (vnode == null || typeof vnode === 'boolean') {
7090
return '';
@@ -81,6 +101,7 @@ function _renderToStringPretty(
81101

82102
if (Array.isArray(vnode)) {
83103
let rendered = '';
104+
parent[CHILDREN] = vnode;
84105
for (let i = 0; i < vnode.length; i++) {
85106
if (pretty && i > 0) rendered = rendered + '\n';
86107
rendered =
@@ -91,7 +112,8 @@ function _renderToStringPretty(
91112
opts,
92113
inner,
93114
isSvgMode,
94-
selectValue
115+
selectValue,
116+
parent
95117
);
96118
}
97119
return rendered;
@@ -100,6 +122,7 @@ function _renderToStringPretty(
100122
// VNodes have {constructor:undefined} to prevent JSON injection:
101123
if (vnode.constructor !== undefined) return '';
102124

125+
vnode[PARENT] = parent;
103126
if (options[DIFF]) options[DIFF](vnode);
104127

105128
let nodeName = vnode.type,
@@ -124,7 +147,8 @@ function _renderToStringPretty(
124147
opts,
125148
opts.shallowHighOrder !== false,
126149
isSvgMode,
127-
selectValue
150+
selectValue,
151+
vnode
128152
);
129153
} else {
130154
let rendered;
@@ -203,7 +227,8 @@ function _renderToStringPretty(
203227
opts,
204228
opts.shallowHighOrder !== false,
205229
isSvgMode,
206-
selectValue
230+
selectValue,
231+
vnode
207232
);
208233

209234
if (options[DIFFED]) options[DIFFED](vnode);
@@ -384,7 +409,8 @@ function _renderToStringPretty(
384409
opts,
385410
true,
386411
childSvgMode,
387-
selectValue
412+
selectValue,
413+
vnode
388414
);
389415

390416
if (shouldPrettyFormatChildren && !hasLarge && isLargeString(ret))

test/pretty.test.jsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import basicRender from '../src/index.js';
22
import { render } from '../src/jsx.js';
33
import { h, Fragment } from 'preact';
4+
import { useId } from 'preact/hooks';
45
import { expect, describe, it } from 'vitest';
56
import { dedent, svgAttributes, htmlAttributes } from './utils.jsx';
67

@@ -267,4 +268,60 @@ describe('pretty', () => {
267268
}
268269
});
269270
});
271+
272+
describe('useId', () => {
273+
it('should produce unique IDs for sibling components', () => {
274+
function Foo() {
275+
const id = useId();
276+
return <div id={id} />;
277+
}
278+
279+
const App = () => {
280+
return (
281+
<>
282+
<Foo />
283+
<Foo />
284+
</>
285+
);
286+
};
287+
288+
const html = render(<App />);
289+
// Each Foo component should have a unique ID
290+
expect(html).to.contain('id="P0-0"');
291+
expect(html).to.contain('id="P0-1"');
292+
});
293+
294+
it('should produce unique IDs in nested components', () => {
295+
function Child() {
296+
const id = useId();
297+
return <span id={id} />;
298+
}
299+
300+
function Parent() {
301+
const id = useId();
302+
return (
303+
<div id={id}>
304+
<Child />
305+
</div>
306+
);
307+
}
308+
309+
const App = () => {
310+
return (
311+
<>
312+
<Parent />
313+
<Parent />
314+
</>
315+
);
316+
};
317+
318+
const html = render(<App />);
319+
// Should have 4 unique IDs total (2 Parents + 2 Children)
320+
const idMatches = html.match(/id="P0-\d+"/g);
321+
expect(idMatches).to.have.length(4);
322+
// All IDs should be unique
323+
const uniqueIds = new Set(idMatches);
324+
expect(uniqueIds.size).to.equal(4);
325+
});
326+
});
270327
});

0 commit comments

Comments
 (0)