wip: support react context

This commit is contained in:
Nate Moore 2023-08-21 16:21:28 -05:00
parent 34c39a0c96
commit 8541e64be5
8 changed files with 131 additions and 50 deletions

View file

@ -1,5 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import './Counter.css'; import './Counter.css';
import { Context } from './Provider.tsx';
export default function Counter({ export default function Counter({
children, children,
@ -11,15 +12,16 @@ export default function Counter({
const [count, setCount] = useState(initialCount); const [count, setCount] = useState(initialCount);
const add = () => setCount((i) => i + 1); const add = () => setCount((i) => i + 1);
const subtract = () => setCount((i) => i - 1); const subtract = () => setCount((i) => i - 1);
console.log('Counter', { count });
return ( return (
<> <Context.Provider value={{ count }}>
<div className="counter"> <div className="counter">
<button onClick={subtract}>-</button> <button onClick={subtract}>-</button>
<pre>{count}</pre> <pre>{count}</pre>
<button onClick={add}>+</button> <button onClick={add}>+</button>
</div> </div>
<div className="counter-message">{children}</div> <div className="counter-message">{children}</div>
</> </Context.Provider>
); );
} }

View file

@ -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 (
<ul>
<li><output>Current count is: {count}</output></li>
<li>{children}</li>
</ul>
);
}

View file

@ -0,0 +1,3 @@
import { createContext } from "react";
export const Context = createContext<{ count: number }>({ count: 0 });

View file

@ -1,8 +1,9 @@
--- ---
// Component Imports // Component Imports
import Counter from '../components/Counter'; import Counter from '../components/Counter';
import Display from '../components/Display';
const someProps = { const someProps = {
count: 0, count: 0,
}; };
// Full Astro Component Syntax: // Full Astro Component Syntax:
@ -10,27 +11,34 @@ const someProps = {
--- ---
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} /> <meta name="generator" content={Astro.generator} />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<style> <style>
html, html,
body { body {
font-family: system-ui; font-family: system-ui;
margin: 0; margin: 0;
} }
body { body {
padding: 2rem; padding: 2rem;
} }
</style> </style>
</head> </head>
<body> <body>
<main> <main>
<Counter {...someProps} client:visible> <Counter {...someProps} client:visible>
<h1>Hello, React!</h1> <div>
</Counter> <h1>Hello, React!</h1>
</main> <Display client:idle>
</body> <Display client:idle>
<Display client:idle />
</Display>
</Display>
</div>
</Counter>
</main>
</body>
</html> </html>

View file

@ -1,6 +1,8 @@
import { createElement, startTransition } from 'react'; import { createElement, startTransition } from 'react';
import { createRoot, hydrateRoot } from 'react-dom/client'; import { createRoot, hydrateRoot } from 'react-dom/client';
import StaticHtml from './static-html.js'; import StaticHtml from './static-html.js';
import { Portal } from './wrapper.js';
import { Fragment } from 'react';
function isAlreadyHydrated(element) { function isAlreadyHydrated(element) {
for (const key in element) { for (const key in element) {
@ -10,31 +12,58 @@ function isAlreadyHydrated(element) {
} }
} }
export default (element) => const app = document.createElement('astro-app');
(Component, props, { default: children, ...slotted }, { client }) => { 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.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 = { const renderOptions = {
identifierPrefix: element.getAttribute('prefix'), 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 });
} }
const componentEl = createElement( const instance = ({ children: _children = [] }) => createElement(Portal, { host: element }, createElement(
Component, Component,
props, props,
children != null ? createElement(StaticHtml, { value: children }) : children children != null ? createElement(StaticHtml, { value: children }) : children,
); ..._children
const rootKey = isAlreadyHydrated(element); ));
// HACK: delete internal react marker for nested components to suppress aggressive warnings
if (rootKey) { instances.set(element, instance);
delete element[rootKey]; instanceChildren.set(instance, [])
if (parentInstance) {
instanceChildren.set(parentInstance, [...instanceChildren.get(parentInstance), instance])
instanceParents.set(instance, parentInstance)
} }
if (client === 'only') { const tree = buildTree();
return startTransition(() => { element.replaceChildren();
createRoot(element).render(componentEl); root.render(createElement(Fragment, {}, ...tree))
}); }
}
return startTransition(() => {
hydrateRoot(element, componentEl, renderOptions);
});
};

View file

@ -27,6 +27,7 @@
"./server-v17.js": "./server-v17.js", "./server-v17.js": "./server-v17.js",
"./package.json": "./package.json", "./package.json": "./package.json",
"./jsx-runtime": "./jsx-runtime.js", "./jsx-runtime": "./jsx-runtime.js",
"./wrapper.js": "./wrapper.js",
"./vnode-children.js": "./vnode-children.js" "./vnode-children.js": "./vnode-children.js"
}, },
"files": [ "files": [
@ -38,6 +39,7 @@
"server.js", "server.js",
"server-v17.js", "server-v17.js",
"static-html.js", "static-html.js",
"wrapper.js",
"vnode-children.js" "vnode-children.js"
], ],
"scripts": { "scripts": {
@ -48,6 +50,7 @@
"dependencies": { "dependencies": {
"@babel/core": "^7.22.5", "@babel/core": "^7.22.5",
"@babel/plugin-transform-react-jsx": "^7.22.5", "@babel/plugin-transform-react-jsx": "^7.22.5",
"its-fine": "^1.1.1",
"ultrahtml": "^1.2.0" "ultrahtml": "^1.2.0"
}, },
"devDependencies": { "devDependencies": {
@ -55,10 +58,10 @@
"@types/react-dom": "^17.0.20", "@types/react-dom": "^17.0.20",
"astro": "workspace:*", "astro": "workspace:*",
"astro-scripts": "workspace:*", "astro-scripts": "workspace:*",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"chai": "^4.3.7", "chai": "^4.3.7",
"cheerio": "1.0.0-rc.12", "cheerio": "1.0.0-rc.12",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"vite": "^4.4.6" "vite": "^4.4.6"
}, },
"peerDependencies": { "peerDependencies": {

View file

@ -0,0 +1,5 @@
import { createPortal } from 'react-dom';
export function Portal({ children, host }) {
return createPortal([children], host);
}

View file

@ -4772,6 +4772,9 @@ importers:
'@babel/plugin-transform-react-jsx': '@babel/plugin-transform-react-jsx':
specifier: ^7.22.5 specifier: ^7.22.5
version: 7.22.5(@babel/core@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: ultrahtml:
specifier: ^1.2.0 specifier: ^1.2.0
version: 1.2.0 version: 1.2.0
@ -9117,6 +9120,12 @@ packages:
'@types/react': 18.2.13 '@types/react': 18.2.13
dev: false 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: /@types/react@17.0.62:
resolution: {integrity: sha512-eANCyz9DG8p/Vdhr0ZKST8JV12PhH2ACCDYlFw6DIO+D+ca+uP4jtEDEpVqXZrh/uZdXQGwk7whJa3ah5DtyLw==} resolution: {integrity: sha512-eANCyz9DG8p/Vdhr0ZKST8JV12PhH2ACCDYlFw6DIO+D+ca+uP4jtEDEpVqXZrh/uZdXQGwk7whJa3ah5DtyLw==}
dependencies: dependencies:
@ -13105,6 +13114,18 @@ packages:
/isexe@2.0.0: /isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 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: /jake@10.8.6:
resolution: {integrity: sha512-G43Ub9IYEFfu72sua6rzooi8V8Gz2lkfk48rW20vEWCGizeaEPlKB1Kh8JIA84yQbiAEfqlPmSpGgCKKxH3rDA==} resolution: {integrity: sha512-G43Ub9IYEFfu72sua6rzooi8V8Gz2lkfk48rW20vEWCGizeaEPlKB1Kh8JIA84yQbiAEfqlPmSpGgCKKxH3rDA==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -18566,25 +18587,21 @@ packages:
file:packages/astro/test/fixtures/css-assets/packages/font-awesome: file:packages/astro/test/fixtures/css-assets/packages/font-awesome:
resolution: {directory: packages/astro/test/fixtures/css-assets/packages/font-awesome, type: directory} resolution: {directory: packages/astro/test/fixtures/css-assets/packages/font-awesome, type: directory}
name: '@test/astro-font-awesome-package' name: '@test/astro-font-awesome-package'
version: 0.0.1
dev: false dev: false
file:packages/astro/test/fixtures/multiple-renderers/renderers/one: file:packages/astro/test/fixtures/multiple-renderers/renderers/one:
resolution: {directory: packages/astro/test/fixtures/multiple-renderers/renderers/one, type: directory} resolution: {directory: packages/astro/test/fixtures/multiple-renderers/renderers/one, type: directory}
name: '@test/astro-renderer-one' name: '@test/astro-renderer-one'
version: 1.0.0
dev: false dev: false
file:packages/astro/test/fixtures/multiple-renderers/renderers/two: file:packages/astro/test/fixtures/multiple-renderers/renderers/two:
resolution: {directory: packages/astro/test/fixtures/multiple-renderers/renderers/two, type: directory} resolution: {directory: packages/astro/test/fixtures/multiple-renderers/renderers/two, type: directory}
name: '@test/astro-renderer-two' name: '@test/astro-renderer-two'
version: 1.0.0
dev: false dev: false
file:packages/astro/test/fixtures/solid-component/deps/solid-jsx-component: 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} resolution: {directory: packages/astro/test/fixtures/solid-component/deps/solid-jsx-component, type: directory}
name: '@test/solid-jsx-component' name: '@test/solid-jsx-component'
version: 0.0.0
dependencies: dependencies:
solid-js: 1.7.6 solid-js: 1.7.6
dev: false dev: false