@astrojs/react
Advanced tools
Comparing version 0.0.0-monorepos-20221010034923 to 0.0.0-perf-20230907211008
import { createElement } from 'react'; | ||
import { render, hydrate } from 'react-dom'; | ||
import { render, hydrate, unmountComponentAtNode } from 'react-dom'; | ||
import StaticHtml from './static-html.js'; | ||
@@ -15,6 +15,9 @@ | ||
); | ||
if (client === 'only') { | ||
return render(componentEl, element); | ||
} | ||
return hydrate(componentEl, element); | ||
const isHydrate = client !== 'only'; | ||
const bootstrap = isHydrate ? hydrate : render; | ||
bootstrap(componentEl, element); | ||
element.addEventListener('astro:unmount', () => unmountComponentAtNode(element), { | ||
once: true, | ||
}); | ||
}; |
@@ -16,2 +16,5 @@ import { createElement, startTransition } from 'react'; | ||
if (!element.hasAttribute('ssr')) return; | ||
const renderOptions = { | ||
identifierPrefix: element.getAttribute('prefix'), | ||
}; | ||
for (const [key, value] of Object.entries(slotted)) { | ||
@@ -26,3 +29,3 @@ props[key] = createElement(StaticHtml, { value, name: key }); | ||
const rootKey = isAlreadyHydrated(element); | ||
// HACK: delete internal react marker for nested components to suppress agressive warnings | ||
// HACK: delete internal react marker for nested components to suppress aggressive warnings | ||
if (rootKey) { | ||
@@ -33,8 +36,12 @@ delete element[rootKey]; | ||
return startTransition(() => { | ||
createRoot(element).render(componentEl); | ||
const root = createRoot(element); | ||
root.render(componentEl); | ||
element.addEventListener('astro:unmount', () => root.unmount(), { once: true }); | ||
}); | ||
} | ||
return startTransition(() => { | ||
hydrateRoot(element, componentEl); | ||
startTransition(() => { | ||
const root = hydrateRoot(element, componentEl, renderOptions); | ||
root.render(componentEl); | ||
element.addEventListener('astro:unmount', () => root.unmount(), { once: true }); | ||
}); | ||
}; |
@@ -1,2 +0,6 @@ | ||
import { AstroIntegration } from 'astro'; | ||
export default function (): AstroIntegration; | ||
import { type Options as ViteReactPluginOptions } from '@vitejs/plugin-react'; | ||
import type { AstroIntegration } from 'astro'; | ||
export type ReactIntegrationOptions = Pick<ViteReactPluginOptions, 'include' | 'exclude'> & { | ||
experimentalReactChildren?: boolean; | ||
}; | ||
export default function ({ include, exclude, experimentalReactChildren, }?: ReactIntegrationOptions): AstroIntegration; |
@@ -0,2 +1,4 @@ | ||
import react, {} from "@vitejs/plugin-react"; | ||
import { version as ReactVersion } from "react-dom"; | ||
const FAST_REFRESH_PREAMBLE = react.preambleCode; | ||
function getRenderer() { | ||
@@ -6,23 +8,31 @@ return { | ||
clientEntrypoint: ReactVersion.startsWith("18.") ? "@astrojs/react/client.js" : "@astrojs/react/client-v17.js", | ||
serverEntrypoint: ReactVersion.startsWith("18.") ? "@astrojs/react/server.js" : "@astrojs/react/server-v17.js", | ||
jsxImportSource: "react", | ||
jsxTransformOptions: async () => { | ||
var _a; | ||
const babelPluginTransformReactJsxModule = await import("@babel/plugin-transform-react-jsx"); | ||
const jsx = ((_a = babelPluginTransformReactJsxModule == null ? void 0 : babelPluginTransformReactJsxModule.default) == null ? void 0 : _a.default) ?? (babelPluginTransformReactJsxModule == null ? void 0 : babelPluginTransformReactJsxModule.default); | ||
return { | ||
plugins: [ | ||
jsx( | ||
{}, | ||
{ | ||
runtime: "automatic", | ||
importSource: ReactVersion.startsWith("18.") ? "react" : "@astrojs/react" | ||
} | ||
) | ||
] | ||
}; | ||
serverEntrypoint: ReactVersion.startsWith("18.") ? "@astrojs/react/server.js" : "@astrojs/react/server-v17.js" | ||
}; | ||
} | ||
function optionsPlugin(experimentalReactChildren) { | ||
const virtualModule = "astro:react:opts"; | ||
const virtualModuleId = "\0" + virtualModule; | ||
return { | ||
name: "@astrojs/react:opts", | ||
resolveId(id) { | ||
if (id === virtualModule) { | ||
return virtualModuleId; | ||
} | ||
}, | ||
load(id) { | ||
if (id === virtualModuleId) { | ||
return { | ||
code: `export default { | ||
experimentalReactChildren: ${JSON.stringify(experimentalReactChildren)} | ||
}` | ||
}; | ||
} | ||
} | ||
}; | ||
} | ||
function getViteConfiguration() { | ||
function getViteConfiguration({ | ||
include, | ||
exclude, | ||
experimentalReactChildren | ||
} = {}) { | ||
return { | ||
@@ -41,4 +51,5 @@ optimizeDeps: { | ||
}, | ||
plugins: [react({ include, exclude }), optionsPlugin(!!experimentalReactChildren)], | ||
resolve: { | ||
dedupe: ["react", "react-dom"] | ||
dedupe: ["react", "react-dom", "react-dom/server"] | ||
}, | ||
@@ -48,5 +59,8 @@ ssr: { | ||
noExternal: [ | ||
// These are all needed to get mui to work. | ||
"@mui/material", | ||
"@mui/base", | ||
"@babel/runtime" | ||
"@babel/runtime", | ||
"redoc", | ||
"use-immer" | ||
] | ||
@@ -56,9 +70,19 @@ } | ||
} | ||
function src_default() { | ||
function src_default({ | ||
include, | ||
exclude, | ||
experimentalReactChildren | ||
} = {}) { | ||
return { | ||
name: "@astrojs/react", | ||
hooks: { | ||
"astro:config:setup": ({ addRenderer, updateConfig }) => { | ||
"astro:config:setup": ({ command, addRenderer, updateConfig, injectScript }) => { | ||
addRenderer(getRenderer()); | ||
updateConfig({ vite: getViteConfiguration() }); | ||
updateConfig({ | ||
vite: getViteConfiguration({ include, exclude, experimentalReactChildren }) | ||
}); | ||
if (command === "dev") { | ||
const preamble = FAST_REFRESH_PREAMBLE.replace(`__BASE__`, "/"); | ||
injectScript("before-hydration", preamble); | ||
} | ||
} | ||
@@ -65,0 +89,0 @@ } |
{ | ||
"name": "@astrojs/react", | ||
"description": "Use React components within Astro", | ||
"version": "0.0.0-monorepos-20221010034923", | ||
"version": "0.0.0-perf-20230907211008", | ||
"type": "module", | ||
@@ -31,15 +31,31 @@ "types": "./dist/index.d.ts", | ||
}, | ||
"files": [ | ||
"dist", | ||
"client.js", | ||
"client-v17.js", | ||
"context.js", | ||
"jsx-runtime.js", | ||
"server.js", | ||
"server-v17.js", | ||
"static-html.js", | ||
"vnode-children.js" | ||
], | ||
"dependencies": { | ||
"@babel/core": ">=7.0.0-0 <8.0.0", | ||
"@babel/plugin-transform-react-jsx": "^7.17.12" | ||
"@vitejs/plugin-react": "^4.0.4", | ||
"ultrahtml": "^1.3.0" | ||
}, | ||
"devDependencies": { | ||
"@types/react": "^17.0.45", | ||
"@types/react-dom": "^17.0.17", | ||
"astro": "1.4.6", | ||
"astro-scripts": "0.0.8", | ||
"@types/react": "^18.2.21", | ||
"@types/react-dom": "^18.2.7", | ||
"chai": "^4.3.7", | ||
"cheerio": "1.0.0-rc.12", | ||
"react": "^18.1.0", | ||
"react-dom": "^18.1.0" | ||
"react-dom": "^18.1.0", | ||
"vite": "^4.4.9", | ||
"astro": "0.0.0-perf-20230907211008", | ||
"astro-scripts": "0.0.14" | ||
}, | ||
"peerDependencies": { | ||
"@types/react": "^17.0.50 || ^18.0.21", | ||
"@types/react-dom": "^17.0.17 || ^18.0.6", | ||
"react": "^17.0.2 || ^18.0.0", | ||
@@ -49,3 +65,3 @@ "react-dom": "^17.0.2 || ^18.0.0" | ||
"engines": { | ||
"node": "^14.18.0 || >=16.12.0" | ||
"node": ">=18.14.1" | ||
}, | ||
@@ -52,0 +68,0 @@ "scripts": { |
# @astrojs/react ⚛️ | ||
This **[Astro integration][astro-integration]** enables server-side rendering and client-side hydration for your [React](https://reactjs.org/) components. | ||
This **[Astro integration][astro-integration]** enables server-side rendering and client-side hydration for your [React](https://react.dev/) components. | ||
@@ -12,2 +12,3 @@ ## Installation | ||
Astro includes a CLI tool for adding first party integrations: `astro add`. This command will: | ||
1. (Optionally) Install all necessary dependencies and peer dependencies | ||
@@ -45,11 +46,11 @@ 2. (Also optionally) Update your `astro.config.*` file to apply this integration | ||
__`astro.config.mjs`__ | ||
```js | ||
```js ins={3} "react()" | ||
// astro.config.mjs | ||
import { defineConfig } from 'astro/config'; | ||
import react from '@astrojs/react'; | ||
export default { | ||
export default defineConfig({ | ||
// ... | ||
integrations: [react()], | ||
} | ||
}); | ||
``` | ||
@@ -60,2 +61,3 @@ | ||
To use your first React component in Astro, head to our [UI framework documentation][astro-ui-frameworks]. You'll explore: | ||
- 📦 how framework components are loaded, | ||
@@ -65,2 +67,75 @@ - 💧 client-side hydration options, and | ||
## Options | ||
### Combining multiple JSX frameworks | ||
When you are using multiple JSX frameworks (React, Preact, Solid) in the same project, Astro needs to determine which JSX framework-specific transformations should be used for each of your components. If you have only added one JSX framework integration to your project, no extra configuration is needed. | ||
Use the `include` (required) and `exclude` (optional) configuration options to specify which files belong to which framework. Provide an array of files and/or folders to `include` for each framework you are using. Wildcards may be used to include multiple file paths. | ||
We recommend placing common framework components in the same folder (e.g. `/components/react/` and `/components/solid/`) to make specifying your includes easier, but this is not required: | ||
```js | ||
import { defineConfig } from 'astro/config'; | ||
import preact from '@astrojs/preact'; | ||
import react from '@astrojs/react'; | ||
import svelte from '@astrojs/svelte'; | ||
import vue from '@astrojs/vue'; | ||
import solid from '@astrojs/solid-js'; | ||
export default defineConfig({ | ||
// Enable many frameworks to support all different kinds of components. | ||
// No `include` is needed if you are only using a single JSX framework! | ||
integrations: [ | ||
preact({ | ||
include: ['**/preact/*'], | ||
}), | ||
react({ | ||
include: ['**/react/*'], | ||
}), | ||
solid({ | ||
include: ['**/solid/*'], | ||
}), | ||
], | ||
}); | ||
``` | ||
### Children parsing | ||
Children passed into a React component from an Astro component are parsed as plain strings, not React nodes. | ||
For example, the `<ReactComponent />` below will only receive a single child element: | ||
```astro | ||
--- | ||
import ReactComponent from './ReactComponent'; | ||
--- | ||
<ReactComponent> | ||
<div>one</div> | ||
<div>two</div> | ||
</ReactComponent> | ||
``` | ||
If you are using a library that _expects_ more than one child element to be passed, for example so that it can slot certain elements in different places, you might find this to be a blocker. | ||
You can set the experimental flag `experimentalReactChildren` to tell Astro to always pass children to React as React vnodes. There is some runtime cost to this, but it can help with compatibility. | ||
You can enable this option in the configuration for the React integration: | ||
```js | ||
// astro.config.mjs | ||
import { defineConfig } from 'astro/config'; | ||
import react from '@astrojs/react'; | ||
export default defineConfig({ | ||
// ... | ||
integrations: [ | ||
react({ | ||
experimentalReactChildren: true, | ||
}), | ||
], | ||
}); | ||
``` | ||
## Troubleshooting | ||
@@ -67,0 +142,0 @@ |
@@ -20,4 +20,3 @@ import React from 'react'; | ||
if (typeof Component === 'object') { | ||
const $$typeof = Component['$$typeof']; | ||
return $$typeof && $$typeof.toString().slice('Symbol('.length).startsWith('react'); | ||
return Component['$$typeof']?.toString().slice('Symbol('.length).startsWith('react'); | ||
} | ||
@@ -66,7 +65,14 @@ if (typeof Component !== 'function') return false; | ||
...slots, | ||
children: children != null ? React.createElement(StaticHtml, { value: children }) : undefined, | ||
}; | ||
const newChildren = children ?? props.children; | ||
if (newChildren != null) { | ||
newProps.children = React.createElement(StaticHtml, { | ||
// Adjust how this is hydrated only when the version of Astro supports `astroStaticSlot` | ||
hydrate: metadata.astroStaticSlot ? !!metadata.hydrate : true, | ||
value: newChildren, | ||
}); | ||
} | ||
const vnode = React.createElement(Component, newProps); | ||
let html; | ||
if (metadata && metadata.hydrate) { | ||
if (metadata?.hydrate) { | ||
html = ReactDOM.renderToString(vnode); | ||
@@ -82,2 +88,3 @@ } else { | ||
renderToStaticMarkup, | ||
supportsAstroStaticSlot: true, | ||
}; |
import React from 'react'; | ||
import ReactDOM from 'react-dom/server'; | ||
import StaticHtml from './static-html.js'; | ||
import { incrementId } from './context.js'; | ||
import opts from 'astro:react:opts'; | ||
@@ -20,4 +22,3 @@ const slotName = (str) => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase()); | ||
if (typeof Component === 'object') { | ||
const $$typeof = Component['$$typeof']; | ||
return $$typeof && $$typeof.toString().slice('Symbol('.length).startsWith('react'); | ||
return Component['$$typeof'].toString().slice('Symbol('.length).startsWith('react'); | ||
} | ||
@@ -61,3 +62,14 @@ if (typeof Component !== 'function') return false; | ||
function needsHydration(metadata) { | ||
// Adjust how this is hydrated only when the version of Astro supports `astroStaticSlot` | ||
return metadata.astroStaticSlot ? !!metadata.hydrate : true; | ||
} | ||
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']; | ||
@@ -67,3 +79,7 @@ const slots = {}; | ||
const name = slotName(key); | ||
slots[name] = React.createElement(StaticHtml, { value, name }); | ||
slots[name] = React.createElement(StaticHtml, { | ||
hydrate: needsHydration(metadata), | ||
value, | ||
name, | ||
}); | ||
} | ||
@@ -75,25 +91,34 @@ // Note: create newProps to avoid mutating `props` before they are serialized | ||
}; | ||
if (children != null) { | ||
newProps.children = React.createElement(StaticHtml, { value: children }); | ||
const newChildren = children ?? props.children; | ||
if (children && opts.experimentalReactChildren) { | ||
const convert = await import('./vnode-children.js').then((mod) => mod.default); | ||
newProps.children = convert(children); | ||
} else if (newChildren != null) { | ||
newProps.children = React.createElement(StaticHtml, { | ||
hydrate: needsHydration(metadata), | ||
value: newChildren, | ||
}); | ||
} | ||
const vnode = React.createElement(Component, newProps); | ||
const renderOptions = { | ||
identifierPrefix: prefix, | ||
}; | ||
let html; | ||
if (metadata && metadata.hydrate) { | ||
html = ReactDOM.renderToString(vnode); | ||
if (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(); | ||
@@ -104,2 +129,3 @@ let html = ''; | ||
let stream = ReactDOM.renderToPipeableStream(vnode, { | ||
...options, | ||
onError(err) { | ||
@@ -126,7 +152,7 @@ error = err; | ||
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) => { | ||
@@ -173,4 +199,4 @@ reject(err); | ||
async function renderToReadableStreamAsync(vnode) { | ||
return await readResult(await ReactDOM.renderToReadableStream(vnode)); | ||
async function renderToReadableStreamAsync(vnode, options) { | ||
return await readResult(await ReactDOM.renderToReadableStream(vnode, options)); | ||
} | ||
@@ -181,2 +207,3 @@ | ||
renderToStaticMarkup, | ||
supportsAstroStaticSlot: true, | ||
}; |
@@ -10,5 +10,6 @@ import { createElement as h } from 'react'; | ||
*/ | ||
const StaticHtml = ({ value, name }) => { | ||
const StaticHtml = ({ value, name, hydrate = true }) => { | ||
if (!value) return null; | ||
return h('astro-slot', { | ||
const tagName = hydrate ? 'astro-slot' : 'astro-static-slot'; | ||
return h(tagName, { | ||
name, | ||
@@ -15,0 +16,0 @@ suppressHydrationWarning: true, |
504
149
26340
6
9
13
+ Added@vitejs/plugin-react@^4.0.4
+ Addedultrahtml@^1.3.0
+ Added@babel/plugin-transform-react-jsx-self@7.25.9(transitive)
+ Added@babel/plugin-transform-react-jsx-source@7.25.9(transitive)
+ Added@esbuild/aix-ppc64@0.24.2(transitive)
+ Added@esbuild/android-arm@0.24.2(transitive)
+ Added@esbuild/android-arm64@0.24.2(transitive)
+ Added@esbuild/android-x64@0.24.2(transitive)
+ Added@esbuild/darwin-arm64@0.24.2(transitive)
+ Added@esbuild/darwin-x64@0.24.2(transitive)
+ Added@esbuild/freebsd-arm64@0.24.2(transitive)
+ Added@esbuild/freebsd-x64@0.24.2(transitive)
+ Added@esbuild/linux-arm@0.24.2(transitive)
+ Added@esbuild/linux-arm64@0.24.2(transitive)
+ Added@esbuild/linux-ia32@0.24.2(transitive)
+ Added@esbuild/linux-loong64@0.24.2(transitive)
+ Added@esbuild/linux-mips64el@0.24.2(transitive)
+ Added@esbuild/linux-ppc64@0.24.2(transitive)
+ Added@esbuild/linux-riscv64@0.24.2(transitive)
+ Added@esbuild/linux-s390x@0.24.2(transitive)
+ Added@esbuild/linux-x64@0.24.2(transitive)
+ Added@esbuild/netbsd-arm64@0.24.2(transitive)
+ Added@esbuild/netbsd-x64@0.24.2(transitive)
+ Added@esbuild/openbsd-arm64@0.24.2(transitive)
+ Added@esbuild/openbsd-x64@0.24.2(transitive)
+ Added@esbuild/sunos-x64@0.24.2(transitive)
+ Added@esbuild/win32-arm64@0.24.2(transitive)
+ Added@esbuild/win32-ia32@0.24.2(transitive)
+ Added@esbuild/win32-x64@0.24.2(transitive)
+ Added@rollup/rollup-android-arm-eabi@4.31.0(transitive)
+ Added@rollup/rollup-android-arm64@4.31.0(transitive)
+ Added@rollup/rollup-darwin-arm64@4.31.0(transitive)
+ Added@rollup/rollup-darwin-x64@4.31.0(transitive)
+ Added@rollup/rollup-freebsd-arm64@4.31.0(transitive)
+ Added@rollup/rollup-freebsd-x64@4.31.0(transitive)
+ Added@rollup/rollup-linux-arm-gnueabihf@4.31.0(transitive)
+ Added@rollup/rollup-linux-arm-musleabihf@4.31.0(transitive)
+ Added@rollup/rollup-linux-arm64-gnu@4.31.0(transitive)
+ Added@rollup/rollup-linux-arm64-musl@4.31.0(transitive)
+ Added@rollup/rollup-linux-loongarch64-gnu@4.31.0(transitive)
+ Added@rollup/rollup-linux-powerpc64le-gnu@4.31.0(transitive)
+ Added@rollup/rollup-linux-riscv64-gnu@4.31.0(transitive)
+ Added@rollup/rollup-linux-s390x-gnu@4.31.0(transitive)
+ Added@rollup/rollup-linux-x64-gnu@4.31.0(transitive)
+ Added@rollup/rollup-linux-x64-musl@4.31.0(transitive)
+ Added@rollup/rollup-win32-arm64-msvc@4.31.0(transitive)
+ Added@rollup/rollup-win32-ia32-msvc@4.31.0(transitive)
+ Added@rollup/rollup-win32-x64-msvc@4.31.0(transitive)
+ Added@types/babel__core@7.20.5(transitive)
+ Added@types/babel__generator@7.6.8(transitive)
+ Added@types/babel__template@7.4.4(transitive)
+ Added@types/babel__traverse@7.20.6(transitive)
+ Added@types/estree@1.0.6(transitive)
+ Added@types/prop-types@15.7.14(transitive)
+ Added@types/react@18.3.18(transitive)
+ Added@types/react-dom@18.3.5(transitive)
+ Added@vitejs/plugin-react@4.3.4(transitive)
+ Addedcsstype@3.1.3(transitive)
+ Addedelectron-to-chromium@1.5.84(transitive)
+ Addedesbuild@0.24.2(transitive)
+ Addedfsevents@2.3.3(transitive)
+ Addednanoid@3.3.8(transitive)
+ Addedpostcss@8.5.1(transitive)
+ Addedreact-refresh@0.14.2(transitive)
+ Addedrollup@4.31.0(transitive)
+ Addedsource-map-js@1.2.1(transitive)
+ Addedultrahtml@1.5.3(transitive)
+ Addedvite@6.0.10(transitive)
- Removed@babel/core@>=7.0.0-0 <8.0.0
- Removed@babel/helper-annotate-as-pure@7.25.9(transitive)
- Removed@babel/plugin-syntax-jsx@7.25.9(transitive)
- Removed@babel/plugin-transform-react-jsx@7.25.9(transitive)
- Removedelectron-to-chromium@1.5.83(transitive)