0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-02-03 22:29:08 -05:00

Generate unique ids within each React island (#6976)

This commit is contained in:
Robin Neal 2023-05-04 15:23:00 +01:00 committed by GitHub
parent dfb9e4270a
commit ca329bbcae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 71 additions and 14 deletions

View file

@ -0,0 +1,5 @@
---
'@astrojs/react': patch
---
Prevent ID collisions in React.useId

View file

@ -0,0 +1,6 @@
import React from 'react';
export default function () {
const id = React.useId();
return <p className='react-use-id' id={id}>{id}</p>;
}

View file

@ -8,6 +8,7 @@ import Pure from '../components/Pure.jsx';
import TypeScriptComponent from '../components/TypeScriptComponent'; import TypeScriptComponent from '../components/TypeScriptComponent';
import CloneElement from '../components/CloneElement'; import CloneElement from '../components/CloneElement';
import WithChildren from '../components/WithChildren'; import WithChildren from '../components/WithChildren';
import WithId from '../components/WithId';
const someProps = { const someProps = {
text: 'Hello world!', text: 'Hello world!',
@ -34,5 +35,7 @@ const someProps = {
<CloneElement /> <CloneElement />
<WithChildren client:load>test</WithChildren> <WithChildren client:load>test</WithChildren>
<WithChildren client:load children="test" /> <WithChildren client:load children="test" />
<WithId client:idle />
<WithId client:idle />
</body> </body>
</html> </html>

View file

@ -42,16 +42,21 @@ describe('React Components', () => {
expect($('#pure')).to.have.lengthOf(1); expect($('#pure')).to.have.lengthOf(1);
// test 8: Check number of islands // test 8: Check number of islands
expect($('astro-island[uid]')).to.have.lengthOf(7); expect($('astro-island[uid]')).to.have.lengthOf(9);
// test 9: Check island deduplication // test 9: Check island deduplication
const uniqueRootUIDs = new Set($('astro-island').map((i, el) => $(el).attr('uid'))); const uniqueRootUIDs = new Set($('astro-island').map((i, el) => $(el).attr('uid')));
expect(uniqueRootUIDs.size).to.equal(6); expect(uniqueRootUIDs.size).to.equal(8);
// test 10: Should properly render children passed as props // test 10: Should properly render children passed as props
const islandsWithChildren = $('.with-children'); const islandsWithChildren = $('.with-children');
expect(islandsWithChildren).to.have.lengthOf(2); expect(islandsWithChildren).to.have.lengthOf(2);
expect($(islandsWithChildren[0]).html()).to.equal($(islandsWithChildren[1]).html()); expect($(islandsWithChildren[0]).html()).to.equal($(islandsWithChildren[1]).html());
// test 11: Should generate unique React.useId per island
const islandsWithId = $('.react-use-id');
expect(islandsWithId).to.have.lengthOf(2);
expect($(islandsWithId[0]).attr('id')).to.not.equal($(islandsWithId[1]).attr('id'))
}); });
it('Can load Vue', async () => { it('Can load Vue', async () => {

View file

@ -13,6 +13,9 @@ function isAlreadyHydrated(element) {
export default (element) => export default (element) =>
(Component, props, { default: children, ...slotted }, { client }) => { (Component, props, { default: children, ...slotted }, { client }) => {
if (!element.hasAttribute('ssr')) return; if (!element.hasAttribute('ssr')) return;
const renderOptions = {
identifierPrefix: element.getAttribute('prefix')
}
for (const [key, value] of Object.entries(slotted)) { for (const [key, value] of Object.entries(slotted)) {
props[key] = createElement(StaticHtml, { value, name: key }); props[key] = createElement(StaticHtml, { value, name: key });
} }
@ -28,10 +31,10 @@ export default (element) =>
} }
if (client === 'only') { if (client === 'only') {
return startTransition(() => { return startTransition(() => {
createRoot(element).render(componentEl); createRoot(element, renderOptions).render(componentEl);
}); });
} }
return startTransition(() => { return startTransition(() => {
hydrateRoot(element, componentEl); hydrateRoot(element, componentEl, renderOptions);
}); });
}; };

View file

@ -0,0 +1,24 @@
const contexts = new WeakMap();
const ID_PREFIX = 'r';
function getContext(rendererContextResult) {
if (contexts.has(rendererContextResult)) {
return contexts.get(rendererContextResult);
}
const ctx = {
currentIndex: 0,
get id() {
return ID_PREFIX + this.currentIndex.toString();
},
};
contexts.set(rendererContextResult, ctx);
return ctx;
}
export function incrementId(rendererContextResult) {
const ctx = getContext(rendererContextResult)
const id = ctx.id;
ctx.currentIndex++;
return id;
}

View file

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/server'; import ReactDOM from 'react-dom/server';
import StaticHtml from './static-html.js'; import StaticHtml from './static-html.js';
import { incrementId } from './context.js';
const slotName = (str) => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase()); const slotName = (str) => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase());
const reactTypeof = Symbol.for('react.element'); const reactTypeof = Symbol.for('react.element');
@ -58,6 +59,12 @@ async function getNodeWritable() {
} }
async function renderToStaticMarkup(Component, props, { default: children, ...slotted }, metadata) { async function renderToStaticMarkup(Component, props, { default: children, ...slotted }, metadata) {
let prefix;
if (this && this.result) {
prefix = incrementId(this.result)
}
const attrs = { prefix };
delete props['class']; delete props['class'];
const slots = {}; const slots = {};
for (const [key, value] of Object.entries(slotted)) { for (const [key, value] of Object.entries(slotted)) {
@ -74,29 +81,33 @@ async function renderToStaticMarkup(Component, props, { default: children, ...sl
newProps.children = React.createElement(StaticHtml, { value: newChildren }); newProps.children = React.createElement(StaticHtml, { value: newChildren });
} }
const vnode = React.createElement(Component, newProps); const vnode = React.createElement(Component, newProps);
const renderOptions = {
identifierPrefix: prefix
}
let html; let html;
if (metadata && metadata.hydrate) { if (metadata && metadata.hydrate) {
if ('renderToReadableStream' in ReactDOM) { if ('renderToReadableStream' in ReactDOM) {
html = await renderToReadableStreamAsync(vnode); html = await renderToReadableStreamAsync(vnode, renderOptions);
} else { } else {
html = await renderToPipeableStreamAsync(vnode); html = await renderToPipeableStreamAsync(vnode, renderOptions);
} }
} else { } else {
if ('renderToReadableStream' in ReactDOM) { if ('renderToReadableStream' in ReactDOM) {
html = await renderToReadableStreamAsync(vnode); html = await renderToReadableStreamAsync(vnode, renderOptions);
} else { } else {
html = await renderToStaticNodeStreamAsync(vnode); html = await renderToStaticNodeStreamAsync(vnode, renderOptions);
} }
} }
return { html }; return { html, attrs };
} }
async function renderToPipeableStreamAsync(vnode) { async function renderToPipeableStreamAsync(vnode, options) {
const Writable = await getNodeWritable(); const Writable = await getNodeWritable();
let html = ''; let html = '';
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let error = undefined; let error = undefined;
let stream = ReactDOM.renderToPipeableStream(vnode, { let stream = ReactDOM.renderToPipeableStream(vnode, {
...options,
onError(err) { onError(err) {
error = err; error = err;
reject(error); reject(error);
@ -118,11 +129,11 @@ async function renderToPipeableStreamAsync(vnode) {
}); });
} }
async function renderToStaticNodeStreamAsync(vnode) { async function renderToStaticNodeStreamAsync(vnode, options) {
const Writable = await getNodeWritable(); const Writable = await getNodeWritable();
let html = ''; let html = '';
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let stream = ReactDOM.renderToStaticNodeStream(vnode); let stream = ReactDOM.renderToStaticNodeStream(vnode, options);
stream.on('error', (err) => { stream.on('error', (err) => {
reject(err); reject(err);
}); });
@ -164,8 +175,8 @@ async function readResult(stream) {
} }
} }
async function renderToReadableStreamAsync(vnode) { async function renderToReadableStreamAsync(vnode, options) {
return await readResult(await ReactDOM.renderToReadableStream(vnode)); return await readResult(await ReactDOM.renderToReadableStream(vnode, options));
} }
export default { export default {