Skip to content

Commit

Permalink
feat(container): client hydration (#11486)
Browse files Browse the repository at this point in the history
* fix: prevent client hydration when rendering via Container API

* revert change that is not needed

* skip client directives via option

* reword changeset

* Fix types of react server.d.ts

* add new API

---------

Co-authored-by: Matthew Phillips <matthew@skypack.dev>
  • Loading branch information
ematipico and matthewp authored Jul 18, 2024
1 parent aa05be3 commit 9c0c849
Show file tree
Hide file tree
Showing 8 changed files with 99 additions and 5 deletions.
15 changes: 15 additions & 0 deletions .changeset/afraid-cups-deliver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'astro': patch
---

Adds a new function called `addClientRenderer` to the Container API.

This function should be used when rendering components using the `client:*` directives. The `addClientRenderer` API must be used
*after* the use of the `addServerRenderer`:

```js
const container = await experimental_AstroContainer.create();
container.addServerRenderer({renderer});
container.addClientRenderer({name: '@astrojs/react', entrypoint: '@astrojs/react/client.js'});
const response = await container.renderToResponse(Component);
```
50 changes: 48 additions & 2 deletions packages/astro/src/container/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
SSRManifest,
SSRResult,
} from '../@types/astro.js';
import { getDefaultClientDirectives } from '../core/client-directive/index.js';
import { ASTRO_CONFIG_DEFAULTS } from '../core/config/schema.js';
import { validateConfig } from '../core/config/validate.js';
import { Logger } from '../core/logger/core.js';
Expand Down Expand Up @@ -96,6 +97,11 @@ export type AddServerRenderer =
name: string;
};

export type AddClientRenderer = {
name: string;
entrypoint: string;
};

function createManifest(
manifest?: AstroContainerManifest,
renderers?: SSRLoadedRenderer[],
Expand All @@ -116,7 +122,7 @@ function createManifest(
entryModules: manifest?.entryModules ?? {},
routes: manifest?.routes ?? [],
adapterName: '',
clientDirectives: manifest?.clientDirectives ?? new Map(),
clientDirectives: manifest?.clientDirectives ?? getDefaultClientDirectives(),
renderers: renderers ?? manifest?.renderers ?? [],
base: manifest?.base ?? ASTRO_CONFIG_DEFAULTS.base,
componentMetadata: manifest?.componentMetadata ?? new Map(),
Expand Down Expand Up @@ -283,7 +289,7 @@ export class experimental_AstroContainer {
}

/**
* Use this function to manually add a renderer to the container.
* Use this function to manually add a **server** renderer to the container.
*
* This function is preferred when you require to use the container with a renderer in environments such as on-demand pages.
*
Expand Down Expand Up @@ -326,6 +332,46 @@ export class experimental_AstroContainer {
}
}

/**
* Use this function to manually add a **client** renderer to the container.
*
* When rendering components that use the `client:*` directives, you need to use this function.
*
* ## Example
*
* ```js
* import reactRenderer from "@astrojs/react/server.js";
* import { experimental_AstroContainer as AstroContainer } from "astro/container"
*
* const container = await AstroContainer.create();
* container.addServerRenderer(reactRenderer);
* container.addClientRenderer({
* name: "@astrojs/react",
* entrypoint: "@astrojs/react/client.js"
* });
* ```
*
* @param options {object}
* @param options.name The name of the renderer. The name **isn't** arbitrary, and it should match the name of the package.
* @param options.entrypoint The entrypoint of the client renderer.
*/
public addClientRenderer(options: AddClientRenderer): void {
const { entrypoint, name } = options;

const rendererIndex = this.#pipeline.manifest.renderers.findIndex((r) => r.name === name);
if (rendererIndex === -1) {
throw new Error(
'You tried to add the ' +
name +
" client renderer, but its server renderer wasn't added. You must add the server renderer first. Use the `addServerRenderer` function."
);
}
const renderer = this.#pipeline.manifest.renderers[rendererIndex];
renderer.clientEntrypoint = entrypoint;

this.#pipeline.manifest.renderers[rendererIndex] = renderer;
}

// NOTE: we keep this private via TS instead via `#` so it's still available on the surface, so we can play with it.
// @ematipico: I plan to use it for a possible integration that could help people
private static async createFromManifest(
Expand Down
5 changes: 4 additions & 1 deletion packages/astro/src/runtime/server/render/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,10 @@ export async function renderComponent(
);

function handleCancellation(e: unknown) {
if (result.cancelled) return { render() {} };
if (result.cancelled)
return {
render() {},
};
throw e;
}
}
Expand Down
9 changes: 9 additions & 0 deletions packages/astro/test/container.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,4 +261,13 @@ describe('Container with renderers', () => {

assert.match(html, /I am a vue button/);
});

it('Should render a component with directives', async () => {
const request = new Request('https://example.com/button-directive');
const response = await app.render(request);
const html = await response.text();

assert.match(html, /Button not rendered/);
assert.match(html, /I am a react button/);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
import Button from "./button.jsx"
---

<div>
<p>Button not rendered</p>
<Button client:idle/>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { APIRoute, SSRLoadedRenderer } from 'astro';
import { experimental_AstroContainer } from 'astro/container';
import renderer from '@astrojs/react/server.js';
import Component from '../components/buttonDirective.astro';

export const GET: APIRoute = async (ctx) => {
const container = await experimental_AstroContainer.create();
container.addServerRenderer({ renderer });
container.addClientRenderer({ name: '@astrojs/react', entrypoint: '@astrojs/react/client.js' });
return await container.renderToResponse(Component);
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {APIRoute, SSRLoadedRenderer} from "astro";
import type {APIRoute} from "astro";
import { experimental_AstroContainer } from "astro/container";
import renderer from '@astrojs/react/server.js';
import Component from "../components/button.jsx"
Expand Down
4 changes: 3 additions & 1 deletion packages/integrations/react/server.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
import type { NamedSSRLoadedRendererValue } from 'astro';
export default NamedSSRLoadedRendererValue;

declare const renderer: NamedSSRLoadedRendererValue;
export default renderer;

0 comments on commit 9c0c849

Please sign in to comment.