Skip to content

Commit 216c185

Browse files
committed
Pass locals to render context and to React’s app entrypoint component
1 parent 7783a6c commit 216c185

File tree

10 files changed

+60
-4
lines changed

10 files changed

+60
-4
lines changed

.changeset/good-tires-say.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'astro': minor
3+
---
4+
5+
Exposes `locals` to the render context so integrations can access them

.changeset/twenty-glasses-cross.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@astrojs/react': minor
3+
---
4+
5+
Passes `Astro.locals` through to `appEntrypoint` as `locals`

packages/astro/src/core/render-context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,7 @@ export class RenderContext {
566566
componentMetadata,
567567
compressHTML,
568568
cookies,
569+
locals: this.locals,
569570
/** This function returns the `Astro` faux-global */
570571
createAstro: (props, slots) => this.createAstro(result, props, slots, ctx),
571572
links,

packages/astro/src/types/public/internal.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ export interface SSRResult {
224224
*/
225225
pathname: string;
226226
cookies: AstroCookies | undefined;
227+
locals: App.Locals;
227228
serverIslandNameMap: Map<string, string>;
228229
trailingSlash: AstroConfig['trailingSlash'];
229230
key: Promise<CryptoKey>;

packages/integrations/react/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ export interface AppEntrypointProps {
3838
* The React island being wrapped.
3939
*/
4040
children?: ReactNode;
41+
/**
42+
* `Astro.locals`, available only in a server environment.
43+
*/
44+
locals?: App.Locals;
4145
}
4246

4347
/**

packages/integrations/react/src/server-v17.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import type { AstroComponentMetadata, NamedSSRLoadedRendererValue } from 'astro'
33
import React from 'react';
44
import ReactDOM from 'react-dom/server';
55
import StaticHtml from './static-html.js';
6+
import type { RendererContext } from './types.js';
67

78
const slotName = (str: string) => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase());
89
const reactTypeof = Symbol.for('react.element');
910

10-
function check(Component: any, props: Record<string, any>, children: any) {
11+
function check(this: RendererContext, Component: any, props: Record<string, any>, children: any) {
1112
// Note: there are packages that do some unholy things to create "components".
1213
// Checking the $$typeof property catches most of these patterns.
1314
if (typeof Component === 'object') {
@@ -32,12 +33,13 @@ function check(Component: any, props: Record<string, any>, children: any) {
3233
return React.createElement('div');
3334
}
3435

35-
renderToStaticMarkup(Tester, props, children, {} as any);
36+
renderToStaticMarkup.call(this, Tester, props, children, {} as any);
3637

3738
return isReactComponent;
3839
}
3940

4041
async function renderToStaticMarkup(
42+
this: RendererContext,
4143
Component: any,
4244
props: Record<string, any>,
4345
{ default: children, ...slotted }: Record<string, any>,
@@ -64,7 +66,7 @@ async function renderToStaticMarkup(
6466
}
6567
const componentElement = React.createElement(Component, newProps);
6668
const vnode = AppEntrypoint
67-
? React.createElement(AppEntrypoint, null, componentElement)
69+
? React.createElement(AppEntrypoint, { locals: this?.result?.locals }, componentElement)
6870
: componentElement;
6971
let html: string;
7072
if (metadata?.hydrate) {

packages/integrations/react/src/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ async function renderToStaticMarkup(
111111
}
112112
const componentElement = React.createElement(Component, newProps);
113113
const vnode = AppEntrypoint
114-
? React.createElement(AppEntrypoint, null, componentElement)
114+
? React.createElement(AppEntrypoint, { locals: this?.result?.locals }, componentElement)
115115
: componentElement;
116116
const renderOptions = {
117117
identifierPrefix: prefix,
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
// Set data on Astro.locals - this will be passed to the wrapper component
3+
Astro.locals.serverData = 'from-locals';
4+
---
5+
<html>
6+
<head>
7+
<title>React App Entrypoint Test</title>
8+
</head>
9+
<body>
10+
<slot />
11+
</body>
12+
</html>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
import Layout from '../layouts/Layout.astro';
3+
import Counter from '../components/Counter';
4+
---
5+
<Layout>
6+
<h1>React App Entrypoint with Locals</h1>
7+
<Counter client:load />
8+
</Layout>

packages/integrations/react/test/react-app-entrypoint.test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ describe('React App Entrypoint', () => {
3232
// The counter should have access to the theme from context
3333
assert.equal(counter.attr('data-theme'), 'dark');
3434
});
35+
36+
it('Passes Astro.locals props to the wrapper', async () => {
37+
const html = await fixture.readFile('/with-locals/index.html');
38+
const $ = cheerioLoad(html);
39+
40+
// The wrapper should receive serverData from Astro.locals
41+
const wrapper = $('[data-testid="wrapper"]');
42+
assert.equal(wrapper.attr('data-server'), 'from-locals');
43+
});
3544
});
3645

3746
if (isWindows) return;
@@ -62,5 +71,14 @@ describe('React App Entrypoint', () => {
6271
// The counter should have access to the theme from context
6372
assert.equal(counter.attr('data-theme'), 'dark');
6473
});
74+
75+
it('Passes Astro.locals props to the wrapper in dev mode', async () => {
76+
const html = await fixture.fetch('/with-locals').then((res) => res.text());
77+
const $ = cheerioLoad(html);
78+
79+
// The wrapper should receive serverData from Astro.locals
80+
const wrapper = $('[data-testid="wrapper"]');
81+
assert.equal(wrapper.attr('data-server'), 'from-locals');
82+
});
6583
});
6684
});

0 commit comments

Comments
 (0)