aboutsummaryrefslogtreecommitdiff
path: root/packages/integrations/react/src/client.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/integrations/react/src/client.ts')
-rw-r--r--packages/integrations/react/src/client.ts121
1 files changed, 121 insertions, 0 deletions
diff --git a/packages/integrations/react/src/client.ts b/packages/integrations/react/src/client.ts
new file mode 100644
index 000000000..ea187a1a1
--- /dev/null
+++ b/packages/integrations/react/src/client.ts
@@ -0,0 +1,121 @@
+import { createElement, startTransition } from 'react';
+import { type Root, createRoot, hydrateRoot } from 'react-dom/client';
+import StaticHtml from './static-html.js';
+
+function isAlreadyHydrated(element: HTMLElement) {
+ for (const key in element) {
+ if (key.startsWith('__reactContainer')) {
+ return key as keyof HTMLElement;
+ }
+ }
+}
+
+function createReactElementFromDOMElement(element: any): any {
+ let attrs: Record<string, string> = {};
+ for (const attr of element.attributes) {
+ attrs[attr.name] = attr.value;
+ }
+ // If the element has no children, we can create a simple React element
+ if (element.firstChild === null) {
+ return createElement(element.localName, attrs);
+ }
+
+ return createElement(
+ element.localName,
+ attrs,
+ Array.from(element.childNodes)
+ .map((c: any) => {
+ if (c.nodeType === Node.TEXT_NODE) {
+ return c.data;
+ } else if (c.nodeType === Node.ELEMENT_NODE) {
+ return createReactElementFromDOMElement(c);
+ } else {
+ return undefined;
+ }
+ })
+ .filter((a) => !!a),
+ );
+}
+
+function getChildren(childString: string, experimentalReactChildren: boolean) {
+ if (experimentalReactChildren && childString) {
+ let children = [];
+ let template = document.createElement('template');
+ template.innerHTML = childString;
+ for (let child of template.content.children) {
+ children.push(createReactElementFromDOMElement(child));
+ }
+ return children;
+ } else if (childString) {
+ return createElement(StaticHtml, { value: childString });
+ } else {
+ return undefined;
+ }
+}
+
+// Keep a map of roots so we can reuse them on re-renders
+let rootMap = new WeakMap<HTMLElement, Root>();
+const getOrCreateRoot = (element: HTMLElement, creator: () => Root) => {
+ let root = rootMap.get(element);
+ if (!root) {
+ root = creator();
+ rootMap.set(element, root);
+ }
+ return root;
+};
+
+export default (element: HTMLElement) =>
+ (
+ Component: any,
+ props: Record<string, any>,
+ { default: children, ...slotted }: Record<string, any>,
+ { client }: Record<string, string>,
+ ) => {
+ if (!element.hasAttribute('ssr')) return;
+
+ const actionKey = element.getAttribute('data-action-key');
+ const actionName = element.getAttribute('data-action-name');
+ const stringifiedActionResult = element.getAttribute('data-action-result');
+
+ const formState =
+ actionKey && actionName && stringifiedActionResult
+ ? [JSON.parse(stringifiedActionResult), actionKey, actionName]
+ : undefined;
+
+ const renderOptions = {
+ identifierPrefix: element.getAttribute('prefix'),
+ formState,
+ };
+ for (const [key, value] of Object.entries(slotted)) {
+ props[key] = createElement(StaticHtml, { value, name: key });
+ }
+
+ const componentEl = createElement(
+ Component,
+ props,
+ getChildren(children, element.hasAttribute('data-react-children')),
+ );
+ const rootKey = isAlreadyHydrated(element);
+ // HACK: delete internal react marker for nested components to suppress aggressive warnings
+ if (rootKey) {
+ delete element[rootKey];
+ }
+ if (client === 'only') {
+ return startTransition(() => {
+ const root = getOrCreateRoot(element, () => {
+ const r = createRoot(element);
+ element.addEventListener('astro:unmount', () => r.unmount(), { once: true });
+ return r;
+ });
+ root.render(componentEl);
+ });
+ }
+ startTransition(() => {
+ const root = getOrCreateRoot(element, () => {
+ const r = hydrateRoot(element, componentEl, renderOptions as any);
+ element.addEventListener('astro:unmount', () => r.unmount(), { once: true });
+ return r;
+ });
+ root.render(componentEl);
+ });
+ };