diff --git a/docs/renderers.md b/docs/renderers.md new file mode 100644 index 0000000000..6787216782 --- /dev/null +++ b/docs/renderers.md @@ -0,0 +1,163 @@ +# 🪄 Renderers + +Astro is able to render [React](https://npm.im/@astrojs/renderer-react), [Svelte](https://npm.im/@astrojs/renderer-svelte), [Vue](https://npm.im/@astrojs/renderer-vue), and [Preact](https://npm.im/@astrojs/renderer-preact) components out of the box. This is because Astro's [default configuration][astro-config] relies on **renderers** for those frameworks. + +If you'd like to add support for another framework, you can build a **renderer** plugin using the same interface as Astro's official renderers. + +## What is a renderer? + +A renderer is an NPM package that has two responsiblities—the first is to _render a component to a static string of HTML_ at build time and the second is to _rehydrate that HTML_ to an interactive component on the client. + +Without getting too much further, it might be helpful to take a look at Astro's built-in [`renderers`](https://github.com/snowpackjs/astro/tree/main/packages/renderers). We'll go into more detail in the following sections. + +## Enabling a new renderer + +To enable a new renderer, add the dependency to your project and update the `renderers` array to include it. + +```diff + export default { + renderers: [ ++ 'my-custom-renderer', + '@astrojs/renderer-svelte', + '@astrojs/renderer-vue', + '@astrojs/renderer-react', + '@astrojs/renderer-preact', + ], + } +``` + +## Building a new renderer + +A simple renderer only needs a few files. +``` +/renderer-xxx/ +├── package.json +├── index.js +├── server.js +└── client.js +``` + +Two quick notes before we dive into these files individually. +1. We'd love for you to contribute any renderer you build directly to the Astro repo. This will allow us to publish it under `@astrojs/renderer-xxx`! Feel free to open a pull request. +2. Your renderer doesn't need to be written in ESM, but it's pretty straightforward! Add `"type": "module"` to your `package.json` file and be sure to [define a valid `export` map](https://nodejs.org/api/packages.html#packages_package_entry_points). + +## Renderer Entrypoint (`index.js`) + +The main entrypoint of a renderer is a simple JS file which exports a manifest for the renderer. The required values are `name`, `server`, and `client`. + +Additionally, this entrypoint can optionally define a [Snowpack plugin](https://www.snowpack.dev/guides/plugins) that should be used to load non-JavaScript files. + + +```js +export default { + name: '@astrojs/renderer-xxx', // the renderer name + client: './client.js', // relative path to the client entrypoint + server: './server.js', // relative path to the server entrypoint + snowpackPlugin: '@snowpack/plugin-xxx', // optional, the name of a snowpack plugin to inject + snowpackPluginOptions: { example: true }, // optional, any options to be forwarded to the snowpack plugin + +}; +``` + +## Server entrypoint (`server.js`) + +The server entrypoint of a renderer is responsible for checking if a component should use this renderer, and if so, how that component should be rendered to a string of static HTML. + +```js +export default { + // should Component use this renderer? + check(Component, props, childHTML) {}, + // Component => string of static HTML + renderToStaticMarkup(Component, props, childHTML) {} +} +``` + +### `check` + +`check` is a function that determines whether a Component should be "claimed" by this renderer. + +In it's simplest form, it can check for the existence of a flag on Object-based components. + +```js +function check(Component) { + return Component.isMyFrameworkComponent; +} +``` + +In more complex scenarios, like when a Component is a `Function` without any flags, you may need to use `try/catch` to attempt a full render. This result is cached so that it only runs once per-component. + +```js +function check(Component, props, childHTML) { + try { + const { html } = renderToStaticMarkup(Component, props, childHTML); + return Boolean(html); + } catch (e) {} + return false; +} +``` + +### `renderToStaticMarkup` + +`renderToStaticMarkup` is a function that renders a Component to a static string of HTML. You don't need to worry about hydration + +```js +import { renderToString } from 'xxx'; + +function renderToStaticMarkup(Component, props, childHTML) { + const html = renderToString(h(Component, { ...props, innerHTML: childHTML })); + return { html }; +} +``` + +Note that `childHTML` is an HTML string representing this component's children. If your framework does not support rendering HTML directly, you are welcome to use a wrapper component. By convention Astro uses a custom element, `astro-fragment`, to inject `childHTML` into. + +```js +import { h, renderToString } from 'xxx'; + +const Wrapper = ({ value }) => h('astro-fragment', { dangerouslySetInnerHTML: { __html: value } }); + +function renderToStaticMarkup(Component, props, childHTML) { + const html = renderToString( + h(Component, props, + h(Wrapper, { value: childHTML }) + ) + ); + return { html }; +} +``` + +## Client entrypoint (`client.js`) + +The client entrypoint of a renderer is responsible for rehydrating a static HTML (the result of `renderToStaticMarkup`) back into a fully interactive component. Its `default` export should be a `function` which accepts the host element of the Component, an `astro-root` custom element. + +> If your framework supports non-destructive component hydration (as opposed to a destructive `render` method), be sure to use that! Following your framework's Server Side Rendering (SSR) guide should point you in the right direction. + +```js +import { hydrate } from 'xxx'; + +export default (element) => { + return (Component, props, childHTML) => { + hydrate(h(Component, { ...props, innerHTML: childHTML })); + } +} +``` + +Note that `childHTML` is an HTML string representing this component's children. If your framework does not support rendering HTML directly, you should use the same wrapper component you used for the server entrypoint. + +```js +import { h, hydrate } from 'xxx'; +import SharedWrapper from './SharedWrapper.js'; + +export default (element) => { + return (Component, props, childHTML) => { + hydrate( + h(Component, props, + h(SharedWrapper, { value: childHTML }) + ) + ); + } +} +``` + + +[astro-config]: ./config.md