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/friendly-queens-juggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'preact-render-to-string': patch
---

Ensure `useId()` produces unique IDs when using the JSX renderer
42 changes: 34 additions & 8 deletions src/pretty.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,16 @@ import {
isDirty,
unsetDirty
} from './lib/util.js';
import { COMMIT, DIFF, DIFFED, RENDER, SKIP_EFFECTS } from './lib/constants.js';
import { options, Fragment } from 'preact';
import {
COMMIT,
DIFF,
DIFFED,
RENDER,
SKIP_EFFECTS,
PARENT,
CHILDREN
} from './lib/constants.js';
import { options, Fragment, h } from 'preact';

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

const parent = h(Fragment, null);
parent[CHILDREN] = [vnode];

try {
return _renderToStringPretty(vnode, context || {}, opts, _inner);
return _renderToStringPretty(
vnode,
context || {},
opts,
_inner,
false,
undefined,
parent
);
} finally {
// options._commit, we don't schedule any effects in this library right now,
// so we can pass an empty queue to this hook.
Expand All @@ -64,7 +83,8 @@ function _renderToStringPretty(
opts,
inner,
isSvgMode,
selectValue
selectValue,
parent
) {
if (vnode == null || typeof vnode === 'boolean') {
return '';
Expand All @@ -81,6 +101,7 @@ function _renderToStringPretty(

if (Array.isArray(vnode)) {
let rendered = '';
parent[CHILDREN] = vnode;
for (let i = 0; i < vnode.length; i++) {
if (pretty && i > 0) rendered = rendered + '\n';
rendered =
Expand All @@ -91,7 +112,8 @@ function _renderToStringPretty(
opts,
inner,
isSvgMode,
selectValue
selectValue,
parent
);
}
return rendered;
Expand All @@ -100,6 +122,7 @@ function _renderToStringPretty(
// VNodes have {constructor:undefined} to prevent JSON injection:
if (vnode.constructor !== undefined) return '';

vnode[PARENT] = parent;
if (options[DIFF]) options[DIFF](vnode);

let nodeName = vnode.type,
Expand All @@ -124,7 +147,8 @@ function _renderToStringPretty(
opts,
opts.shallowHighOrder !== false,
isSvgMode,
selectValue
selectValue,
vnode
);
} else {
let rendered;
Expand Down Expand Up @@ -203,7 +227,8 @@ function _renderToStringPretty(
opts,
opts.shallowHighOrder !== false,
isSvgMode,
selectValue
selectValue,
vnode
);

if (options[DIFFED]) options[DIFFED](vnode);
Expand Down Expand Up @@ -384,7 +409,8 @@ function _renderToStringPretty(
opts,
true,
childSvgMode,
selectValue
selectValue,
vnode
);

if (shouldPrettyFormatChildren && !hasLarge && isLargeString(ret))
Expand Down
57 changes: 57 additions & 0 deletions test/pretty.test.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import basicRender from '../src/index.js';
import { render } from '../src/jsx.js';
import { h, Fragment } from 'preact';
import { useId } from 'preact/hooks';
import { expect, describe, it } from 'vitest';
import { dedent, svgAttributes, htmlAttributes } from './utils.jsx';

Expand Down Expand Up @@ -267,4 +268,60 @@ describe('pretty', () => {
}
});
});

describe('useId', () => {
it('should produce unique IDs for sibling components', () => {
function Foo() {
const id = useId();
return <div id={id} />;
}

const App = () => {
return (
<>
<Foo />
<Foo />
</>
);
};

const html = render(<App />);
// Each Foo component should have a unique ID
expect(html).to.contain('id="P0-0"');
expect(html).to.contain('id="P0-1"');
});

it('should produce unique IDs in nested components', () => {
function Child() {
const id = useId();
return <span id={id} />;
}

function Parent() {
const id = useId();
return (
<div id={id}>
<Child />
</div>
);
}

const App = () => {
return (
<>
<Parent />
<Parent />
</>
);
};

const html = render(<App />);
// Should have 4 unique IDs total (2 Parents + 2 Children)
const idMatches = html.match(/id="P0-\d+"/g);
expect(idMatches).to.have.length(4);
// All IDs should be unique
const uniqueIds = new Set(idMatches);
expect(uniqueIds.size).to.equal(4);
});
});
});