diff --git a/.changeset/happy-ears-call.md b/.changeset/happy-ears-call.md new file mode 100644 index 0000000000000..0287a7f28eb44 --- /dev/null +++ b/.changeset/happy-ears-call.md @@ -0,0 +1,5 @@ +--- +'@astrojs/react': minor +--- + +Prevent ID collisions in React.useId diff --git a/packages/integrations/react/client.js b/packages/integrations/react/client.js index 3807ab41069f4..366d499e3ad75 100644 --- a/packages/integrations/react/client.js +++ b/packages/integrations/react/client.js @@ -13,6 +13,9 @@ function isAlreadyHydrated(element) { export default (element) => (Component, props, { default: children, ...slotted }, { client }) => { if (!element.hasAttribute('ssr')) return; + const renderOptions = { + identifierPrefix: element.getAttribute('prefix') + } for (const [key, value] of Object.entries(slotted)) { props[key] = createElement(StaticHtml, { value, name: key }); } @@ -28,10 +31,10 @@ export default (element) => } if (client === 'only') { return startTransition(() => { - createRoot(element).render(componentEl); + createRoot(element, renderOptions).render(componentEl); }); } return startTransition(() => { - hydrateRoot(element, componentEl); + hydrateRoot(element, componentEl, renderOptions); }); }; diff --git a/packages/integrations/react/context.js b/packages/integrations/react/context.js new file mode 100644 index 0000000000000..c999c53d3a68f --- /dev/null +++ b/packages/integrations/react/context.js @@ -0,0 +1,21 @@ +const contexts = new WeakMap(); + +export function getContext(result) { + if (contexts.has(result)) { + return contexts.get(result); + } + let ctx = { + c: 0, + get id() { + return 'r' + this.c.toString(); + }, + }; + contexts.set(result, ctx); + return ctx; +} + +export function incrementId(ctx) { + let id = ctx.id; + ctx.c++; + return id; +} diff --git a/packages/integrations/react/server.js b/packages/integrations/react/server.js index 01a135a9ba4c0..f9c03e2250831 100644 --- a/packages/integrations/react/server.js +++ b/packages/integrations/react/server.js @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom/server'; import StaticHtml from './static-html.js'; +import { getContext, incrementId } from './context.js'; const slotName = (str) => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase()); const reactTypeof = Symbol.for('react.element'); @@ -58,6 +59,12 @@ async function getNodeWritable() { } async function renderToStaticMarkup(Component, props, { default: children, ...slotted }, metadata) { + let prefix; + if (this && this.result) { + prefix = incrementId(getContext(this.result)) + } + const attrs = { prefix }; + delete props['class']; const slots = {}; 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 }); } const vnode = React.createElement(Component, newProps); + const renderOptions = { + identifierPrefix: prefix + } let html; if (metadata && metadata.hydrate) { if ('renderToReadableStream' in ReactDOM) { - html = await renderToReadableStreamAsync(vnode); + html = await renderToReadableStreamAsync(vnode, renderOptions); } else { - html = await renderToPipeableStreamAsync(vnode); + html = await renderToPipeableStreamAsync(vnode, renderOptions); } } else { if ('renderToReadableStream' in ReactDOM) { - html = await renderToReadableStreamAsync(vnode); + html = await renderToReadableStreamAsync(vnode, renderOptions); } 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(); let html = ''; return new Promise((resolve, reject) => { let error = undefined; let stream = ReactDOM.renderToPipeableStream(vnode, { + ...options, onError(err) { error = err; reject(error); @@ -118,11 +129,11 @@ async function renderToPipeableStreamAsync(vnode) { }); } -async function renderToStaticNodeStreamAsync(vnode) { +async function renderToStaticNodeStreamAsync(vnode, options) { const Writable = await getNodeWritable(); let html = ''; return new Promise((resolve, reject) => { - let stream = ReactDOM.renderToStaticNodeStream(vnode); + let stream = ReactDOM.renderToStaticNodeStream(vnode, options); stream.on('error', (err) => { reject(err); }); @@ -164,8 +175,8 @@ async function readResult(stream) { } } -async function renderToReadableStreamAsync(vnode) { - return await readResult(await ReactDOM.renderToReadableStream(vnode)); +async function renderToReadableStreamAsync(vnode, options) { + return await readResult(await ReactDOM.renderToReadableStream(vnode, options)); } export default {