Skip to content

Commit 2912072

Browse files
committed
Render empty string when encountering portals
1 parent b283245 commit 2912072

File tree

3 files changed

+77
-1
lines changed

3 files changed

+77
-1
lines changed

.changeset/loud-plums-pay.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+
Render an empty string when encountering portals

src/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,13 @@ function _renderToString(
303303

304304
// Invoke rendering on Components
305305
if (typeof type == 'function') {
306+
// Portals: createPortal() sets `containerInfo` on the vnode.
307+
// In SSR there is no DOM to portal into, so we just render an
308+
// empty placeholder and skip the component entirely.
309+
if ('containerInfo' in vnode) {
310+
return '';
311+
}
312+
306313
let cctx = context,
307314
contextType,
308315
rendered,

test/compat/index.test.jsx

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,72 @@
11
import render from '../../src/index.js';
2-
import { createElement as h, Component } from 'preact/compat';
2+
import { createElement as h, Component, createPortal } from 'preact/compat';
33
import { expect, describe, it } from 'vitest';
44

55
describe('compat', () => {
6+
describe('createPortal', () => {
7+
it('should render portal children inline', () => {
8+
const container = { nodeType: 1 };
9+
const rendered = render(
10+
<div>
11+
<span>before</span>
12+
{createPortal(<div class="portal-content">portaled</div>, container)}
13+
<span>after</span>
14+
</div>
15+
);
16+
expect(rendered).to.equal(
17+
'<div><span>before</span><div class="portal-content"></div><span>after</span></div>'
18+
);
19+
});
20+
21+
it('should render nested portals', () => {
22+
const container1 = { nodeType: 1 };
23+
const container2 = { nodeType: 1 };
24+
const rendered = render(
25+
<div>
26+
{createPortal(
27+
<div class="outer">
28+
{createPortal(<div class="inner">nested</div>, container2)}
29+
</div>,
30+
container1
31+
)}
32+
</div>
33+
);
34+
expect(rendered).to.equal(
35+
'<div><div class="outer"><div class="inner">nested</div></div></div>'
36+
);
37+
});
38+
39+
it('should render portal with multiple children', () => {
40+
const container = { nodeType: 1 };
41+
const rendered = render(
42+
<div>
43+
{createPortal(
44+
[<span key="a">first</span>, <span key="b">second</span>],
45+
container
46+
)}
47+
</div>
48+
);
49+
expect(rendered).to.equal('<div></div>');
50+
});
51+
52+
it('should render portal with text content', () => {
53+
const container = { nodeType: 1 };
54+
const rendered = render(
55+
<div>{createPortal('just text', container)}</div>
56+
);
57+
expect(rendered).to.equal('<div></div>');
58+
});
59+
60+
it('should render portal with component children', () => {
61+
const container = { nodeType: 1 };
62+
function Inner() {
63+
return <em>component inside portal</em>;
64+
}
65+
const rendered = render(<div>{createPortal(<Inner />, container)}</div>);
66+
expect(rendered).to.equal('<div></div>');
67+
});
68+
});
69+
670
it('should not duplicate class attribute when className is empty', async () => {
771
let rendered = render(h('div', { className: '' }));
872
let expected = `<div></div>`;

0 commit comments

Comments
 (0)