diff --git a/examples/framework-react/src/components/Counter.tsx b/examples/framework-react/src/components/Counter.tsx
index cc416d3f1..40f1e11ef 100644
--- a/examples/framework-react/src/components/Counter.tsx
+++ b/examples/framework-react/src/components/Counter.tsx
@@ -1,5 +1,6 @@
import { useState } from 'react';
import './Counter.css';
+import { Context } from './Provider.tsx';
export default function Counter({
children,
@@ -11,15 +12,16 @@ export default function Counter({
const [count, setCount] = useState(initialCount);
const add = () => setCount((i) => i + 1);
const subtract = () => setCount((i) => i - 1);
+ console.log('Counter', { count });
return (
- <>
+
{children}
- >
+
);
}
diff --git a/examples/framework-react/src/components/Display.tsx b/examples/framework-react/src/components/Display.tsx
new file mode 100644
index 000000000..43485d919
--- /dev/null
+++ b/examples/framework-react/src/components/Display.tsx
@@ -0,0 +1,14 @@
+import { useContext } from 'react';
+import { Context } from './Provider.tsx';
+
+export default function Display({ children }) {
+ const { count } = useContext(Context);
+ console.log('Display', { count });
+
+ return (
+
+
+ - {children}
+
+ );
+}
diff --git a/examples/framework-react/src/components/Provider.tsx b/examples/framework-react/src/components/Provider.tsx
new file mode 100644
index 000000000..6a8881ba7
--- /dev/null
+++ b/examples/framework-react/src/components/Provider.tsx
@@ -0,0 +1,3 @@
+import { createContext } from "react";
+
+export const Context = createContext<{ count: number }>({ count: 0 });
diff --git a/examples/framework-react/src/pages/index.astro b/examples/framework-react/src/pages/index.astro
index cd7bdc52e..4e54cf689 100644
--- a/examples/framework-react/src/pages/index.astro
+++ b/examples/framework-react/src/pages/index.astro
@@ -1,8 +1,9 @@
---
// Component Imports
import Counter from '../components/Counter';
+import Display from '../components/Display';
const someProps = {
- count: 0,
+ count: 0,
};
// Full Astro Component Syntax:
@@ -10,27 +11,34 @@ const someProps = {
---
-
-
-
-
-
-
-
-
-
-
- Hello, React!
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
Hello, React!
+
+
+
+
+
+
+
+
+
diff --git a/packages/integrations/react/client.js b/packages/integrations/react/client.js
index d8948e7bb..696a34110 100644
--- a/packages/integrations/react/client.js
+++ b/packages/integrations/react/client.js
@@ -1,6 +1,8 @@
import { createElement, startTransition } from 'react';
import { createRoot, hydrateRoot } from 'react-dom/client';
import StaticHtml from './static-html.js';
+import { Portal } from './wrapper.js';
+import { Fragment } from 'react';
function isAlreadyHydrated(element) {
for (const key in element) {
@@ -10,31 +12,58 @@ function isAlreadyHydrated(element) {
}
}
-export default (element) =>
- (Component, props, { default: children, ...slotted }, { client }) => {
+const app = document.createElement('astro-app');
+const root = createRoot(app)
+const instances = new Map();
+const instanceChildren = new Map();
+const instanceParents = new Map();
+
+function buildTree() {
+ const vnodes = [];
+ function addNode(node) {
+ const parent = instanceParents.get(node);
+ const children = instanceChildren.get(node);
+ const self = node({ children: children.map(child => addNode(child) )});
+ if (!parent) {
+ vnodes.push(self);
+ } else {
+ return self;
+ }
+ }
+ for (const i of instances.values()) {
+ addNode(i);
+ }
+ return vnodes;
+}
+
+export default (element) => (Component, props, { default: children, ...slotted }, meta) => {
if (!element.hasAttribute('ssr')) return;
+ if (element.parentElement.closest('astro-island[ssr]')) return;
+
+ const parentElement = element.parentElement.closest('astro-island');
+ let parentInstance = null;
+ if (parentElement) parentInstance = instances.get(parentElement);
+
const renderOptions = {
identifierPrefix: element.getAttribute('prefix'),
};
for (const [key, value] of Object.entries(slotted)) {
props[key] = createElement(StaticHtml, { value, name: key });
}
- const componentEl = createElement(
+ const instance = ({ children: _children = [] }) => createElement(Portal, { host: element }, createElement(
Component,
props,
- children != null ? createElement(StaticHtml, { value: children }) : children
- );
- const rootKey = isAlreadyHydrated(element);
- // HACK: delete internal react marker for nested components to suppress aggressive warnings
- if (rootKey) {
- delete element[rootKey];
+ children != null ? createElement(StaticHtml, { value: children }) : children,
+ ..._children
+ ));
+
+ instances.set(element, instance);
+ instanceChildren.set(instance, [])
+ if (parentInstance) {
+ instanceChildren.set(parentInstance, [...instanceChildren.get(parentInstance), instance])
+ instanceParents.set(instance, parentInstance)
}
- if (client === 'only') {
- return startTransition(() => {
- createRoot(element).render(componentEl);
- });
- }
- return startTransition(() => {
- hydrateRoot(element, componentEl, renderOptions);
- });
- };
+ const tree = buildTree();
+ element.replaceChildren();
+ root.render(createElement(Fragment, {}, ...tree))
+ }
diff --git a/packages/integrations/react/package.json b/packages/integrations/react/package.json
index 682811c7c..c33921800 100644
--- a/packages/integrations/react/package.json
+++ b/packages/integrations/react/package.json
@@ -27,6 +27,7 @@
"./server-v17.js": "./server-v17.js",
"./package.json": "./package.json",
"./jsx-runtime": "./jsx-runtime.js",
+ "./wrapper.js": "./wrapper.js",
"./vnode-children.js": "./vnode-children.js"
},
"files": [
@@ -38,6 +39,7 @@
"server.js",
"server-v17.js",
"static-html.js",
+ "wrapper.js",
"vnode-children.js"
],
"scripts": {
@@ -48,6 +50,7 @@
"dependencies": {
"@babel/core": "^7.22.5",
"@babel/plugin-transform-react-jsx": "^7.22.5",
+ "its-fine": "^1.1.1",
"ultrahtml": "^1.2.0"
},
"devDependencies": {
@@ -55,10 +58,10 @@
"@types/react-dom": "^17.0.20",
"astro": "workspace:*",
"astro-scripts": "workspace:*",
- "react": "^18.1.0",
- "react-dom": "^18.1.0",
"chai": "^4.3.7",
"cheerio": "1.0.0-rc.12",
+ "react": "^18.1.0",
+ "react-dom": "^18.1.0",
"vite": "^4.4.6"
},
"peerDependencies": {
diff --git a/packages/integrations/react/wrapper.js b/packages/integrations/react/wrapper.js
new file mode 100644
index 000000000..e52a2db22
--- /dev/null
+++ b/packages/integrations/react/wrapper.js
@@ -0,0 +1,5 @@
+import { createPortal } from 'react-dom';
+
+export function Portal({ children, host }) {
+ return createPortal([children], host);
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 43f0707d0..e691a1c95 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4772,6 +4772,9 @@ importers:
'@babel/plugin-transform-react-jsx':
specifier: ^7.22.5
version: 7.22.5(@babel/core@7.22.5)
+ its-fine:
+ specifier: ^1.1.1
+ version: 1.1.1(react@18.2.0)
ultrahtml:
specifier: ^1.2.0
version: 1.2.0
@@ -9117,6 +9120,12 @@ packages:
'@types/react': 18.2.13
dev: false
+ /@types/react-reconciler@0.28.2:
+ resolution: {integrity: sha512-8tu6lHzEgYPlfDf/J6GOQdIc+gs+S2yAqlby3zTsB3SP2svlqTYe5fwZNtZyfactP74ShooP2vvi1BOp9ZemWw==}
+ dependencies:
+ '@types/react': 18.2.13
+ dev: false
+
/@types/react@17.0.62:
resolution: {integrity: sha512-eANCyz9DG8p/Vdhr0ZKST8JV12PhH2ACCDYlFw6DIO+D+ca+uP4jtEDEpVqXZrh/uZdXQGwk7whJa3ah5DtyLw==}
dependencies:
@@ -13105,6 +13114,18 @@ packages:
/isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+ /its-fine@1.1.1(react@18.2.0):
+ resolution: {integrity: sha512-v1Ia1xl20KbuSGlwoaGsW0oxsw8Be+TrXweidxD9oT/1lAh6O3K3/GIM95Tt6WCiv6W+h2M7RB1TwdoAjQyyKw==}
+ peerDependencies:
+ react: '>=18.0'
+ peerDependenciesMeta:
+ react:
+ optional: true
+ dependencies:
+ '@types/react-reconciler': 0.28.2
+ react: 18.2.0
+ dev: false
+
/jake@10.8.6:
resolution: {integrity: sha512-G43Ub9IYEFfu72sua6rzooi8V8Gz2lkfk48rW20vEWCGizeaEPlKB1Kh8JIA84yQbiAEfqlPmSpGgCKKxH3rDA==}
engines: {node: '>=10'}
@@ -18566,25 +18587,21 @@ packages:
file:packages/astro/test/fixtures/css-assets/packages/font-awesome:
resolution: {directory: packages/astro/test/fixtures/css-assets/packages/font-awesome, type: directory}
name: '@test/astro-font-awesome-package'
- version: 0.0.1
dev: false
file:packages/astro/test/fixtures/multiple-renderers/renderers/one:
resolution: {directory: packages/astro/test/fixtures/multiple-renderers/renderers/one, type: directory}
name: '@test/astro-renderer-one'
- version: 1.0.0
dev: false
file:packages/astro/test/fixtures/multiple-renderers/renderers/two:
resolution: {directory: packages/astro/test/fixtures/multiple-renderers/renderers/two, type: directory}
name: '@test/astro-renderer-two'
- version: 1.0.0
dev: false
file:packages/astro/test/fixtures/solid-component/deps/solid-jsx-component:
resolution: {directory: packages/astro/test/fixtures/solid-component/deps/solid-jsx-component, type: directory}
name: '@test/solid-jsx-component'
- version: 0.0.0
dependencies:
solid-js: 1.7.6
dev: false