diff --git a/.changeset/wicked-gifts-cover.md b/.changeset/wicked-gifts-cover.md
new file mode 100644
index 000000000..0680e0651
--- /dev/null
+++ b/.changeset/wicked-gifts-cover.md
@@ -0,0 +1,37 @@
+---
+'astro': minor
+---
+
+Adds support for the client:media hydrator
+
+The new `client:media` hydrator allows you to define a component that should only be loaded when a media query matches. An example usage:
+
+```jsx
+---
+import Sidebar from '../components/Sidebar.jsx';
+---
+
+
+```
+
+This allows you to define components which, for example, only run on mobile devices. A common example is a slide-in sidebar that is needed to add navigation to a mobile app, but is never displayed in desktop view.
+
+Since Astro components can have expressions, you can move common media queries to a module for sharing. For example here are defining:
+
+__media.js__
+
+```js
+export const MOBILE = "(max-width: 700px)";
+```
+
+And then you can reference this in your page:
+
+__index.astro__
+
+```jsx
+import Sidebar from '../components/Sidebar.jsx';
+import { MOBILE } from '../media.js';
+---
+
+
+```
\ No newline at end of file
diff --git a/docs/core-concepts/component-hydration.md b/docs/core-concepts/component-hydration.md
index 240eab25b..209e4a8ed 100644
--- a/docs/core-concepts/component-hydration.md
+++ b/docs/core-concepts/component-hydration.md
@@ -30,6 +30,7 @@ Astro renders every component on the server **at build time**. To hydrate any se
- `` will hydrate the component on page load.
- `` will use [requestIdleCallback()][mdn-ric] to hydrate the component as soon as main thread is free.
- `` will use an [IntersectionObserver][mdn-io] to hydrate the component when the element enters the viewport.
+- `` will use [matchMedia][mdn-mm] to hydrate the component when a media query is matched.
## Hydrate Astro Components
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index ce2e2bb03..3ff0ceb7e 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -180,9 +180,10 @@ export type Components = Map;
export interface AstroComponentMetadata {
displayName: string;
- hydrate?: 'load' | 'idle' | 'visible';
+ hydrate?: 'load' | 'idle' | 'visible' | 'media';
componentUrl?: string;
componentExport?: { value: string; namespace?: boolean };
+ value?: undefined | string;
}
type AsyncRendererComponentFn = (Component: any, props: any, children: string | undefined, metadata?: AstroComponentMetadata) => Promise;
diff --git a/packages/astro/src/@types/hydrate.ts b/packages/astro/src/@types/hydrate.ts
index ff1ab0781..88d6a0cc3 100644
--- a/packages/astro/src/@types/hydrate.ts
+++ b/packages/astro/src/@types/hydrate.ts
@@ -1 +1,5 @@
export type GetHydrateCallback = () => Promise<(element: Element, innerHTML: string | null) => void>;
+
+export interface HydrateOptions {
+ value?: string;
+}
\ No newline at end of file
diff --git a/packages/astro/src/compiler/codegen/index.ts b/packages/astro/src/compiler/codegen/index.ts
index f264b5878..52f7fd576 100644
--- a/packages/astro/src/compiler/codegen/index.ts
+++ b/packages/astro/src/compiler/codegen/index.ts
@@ -48,20 +48,25 @@ interface CodeGenOptions {
}
interface HydrationAttributes {
- method?: 'load' | 'idle' | 'visible';
+ method?: 'load' | 'idle' | 'visible' | 'media';
+ value?: undefined | string
}
/** Searches through attributes to extract hydration-rlated attributes */
function findHydrationAttributes(attrs: Record): HydrationAttributes {
let method: HydrationAttributes['method'];
+ let value: undefined | string;
- const hydrationDirectives = new Set(['client:load', 'client:idle', 'client:visible']);
+ const hydrationDirectives = new Set(['client:load', 'client:idle', 'client:visible', 'client:media']);
for (const [key, val] of Object.entries(attrs)) {
- if (hydrationDirectives.has(key)) method = key.slice(7) as HydrationAttributes['method'];
+ if (hydrationDirectives.has(key)) {
+ method = key.slice(7) as HydrationAttributes['method'];
+ value = val === "true" ? undefined : val;
+ }
}
- return { method };
+ return { method, value };
}
/** Retrieve attributes from TemplateNode */
@@ -220,15 +225,17 @@ function getComponentWrapper(_name: string, hydration: HydrationAttributes, { ur
}
};
- const importInfo = method
- ? {
- componentUrl: getComponentUrl(astroConfig, url, pathToFileURL(filename)),
- componentExport: getComponentExport(),
- }
- : {};
+ let metadata: string = '';
+ if(method) {
+ const componentUrl = getComponentUrl(astroConfig, url, pathToFileURL(filename));
+ const componentExport = getComponentExport();
+ metadata = `{ hydrate: "${method}", displayName: "${name}", componentUrl: "${componentUrl}", componentExport: ${JSON.stringify(componentExport)}, value: ${hydration.value || 'null'} }`;
+ } else {
+ metadata = `{ hydrate: undefined, displayName: "${name}", value: ${hydration.value || 'null'} }`
+ }
return {
- wrapper: `__astro_component(${name}, ${JSON.stringify({ hydrate: method, displayName: _name, ...importInfo })})`,
+ wrapper: `__astro_component(${name}, ${metadata})`,
wrapperImports: [`import {__astro_component} from 'astro/dist/internal/__astro_component.js';`],
};
}
diff --git a/packages/astro/src/frontend/hydrate/idle.ts b/packages/astro/src/frontend/hydrate/idle.ts
index 2fd96b9cb..f270d0928 100644
--- a/packages/astro/src/frontend/hydrate/idle.ts
+++ b/packages/astro/src/frontend/hydrate/idle.ts
@@ -1,10 +1,10 @@
-import type { GetHydrateCallback } from '../../@types/hydrate';
+import type { GetHydrateCallback, HydrateOptions } from '../../@types/hydrate';
/**
* Hydrate this component as soon as the main thread is free
* (or after a short delay, if `requestIdleCallback`) isn't supported
*/
-export default async function onIdle(astroId: string, getHydrateCallback: GetHydrateCallback) {
+export default async function onIdle(astroId: string, _options: HydrateOptions, getHydrateCallback: GetHydrateCallback) {
const cb = async () => {
const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`);
const innerHTML = roots[0].querySelector(`astro-fragment`)?.innerHTML ?? null;
diff --git a/packages/astro/src/frontend/hydrate/load.ts b/packages/astro/src/frontend/hydrate/load.ts
index 38ac1a0ea..62e90b660 100644
--- a/packages/astro/src/frontend/hydrate/load.ts
+++ b/packages/astro/src/frontend/hydrate/load.ts
@@ -1,9 +1,9 @@
-import type { GetHydrateCallback } from '../../@types/hydrate';
+import type { GetHydrateCallback, HydrateOptions } from '../../@types/hydrate';
/**
* Hydrate this component immediately
*/
-export default async function onLoad(astroId: string, getHydrateCallback: GetHydrateCallback) {
+export default async function onLoad(astroId: string, _options: HydrateOptions, getHydrateCallback: GetHydrateCallback) {
const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`);
const innerHTML = roots[0].querySelector(`astro-fragment`)?.innerHTML ?? null;
const hydrate = await getHydrateCallback();
diff --git a/packages/astro/src/frontend/hydrate/media.ts b/packages/astro/src/frontend/hydrate/media.ts
new file mode 100644
index 000000000..39c57c4f9
--- /dev/null
+++ b/packages/astro/src/frontend/hydrate/media.ts
@@ -0,0 +1,23 @@
+import type { GetHydrateCallback, HydrateOptions } from '../../@types/hydrate';
+
+/**
+ * Hydrate this component when a matching media query is found
+ */
+export default async function onMedia(astroId: string, options: HydrateOptions, getHydrateCallback: GetHydrateCallback) {
+ const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`);
+ const innerHTML = roots[0].querySelector(`astro-fragment`)?.innerHTML ?? null;
+
+ const cb = async () => {
+ const hydrate = await getHydrateCallback();
+ for (const root of roots) {
+ hydrate(root, innerHTML);
+ }
+ };
+
+ const mql = matchMedia(options.value!);
+ if(mql.matches) {
+ cb();
+ } else {
+ mql.addEventListener('change', cb, {once:true});
+ }
+}
diff --git a/packages/astro/src/frontend/hydrate/visible.ts b/packages/astro/src/frontend/hydrate/visible.ts
index d4dacdf51..eb55e63e9 100644
--- a/packages/astro/src/frontend/hydrate/visible.ts
+++ b/packages/astro/src/frontend/hydrate/visible.ts
@@ -1,11 +1,11 @@
-import type { GetHydrateCallback } from '../../@types/hydrate';
+import type { GetHydrateCallback, HydrateOptions } from '../../@types/hydrate';
/**
* Hydrate this component when one of it's children becomes visible.
* We target the children because `astro-root` is set to `display: contents`
* which doesn't work with IntersectionObserver
*/
-export default async function onVisible(astroId: string, getHydrateCallback: GetHydrateCallback) {
+export default async function onVisible(astroId: string, _options: HydrateOptions, getHydrateCallback: GetHydrateCallback) {
const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`);
const innerHTML = roots[0].querySelector(`astro-fragment`)?.innerHTML ?? null;
diff --git a/packages/astro/src/internal/__astro_component.ts b/packages/astro/src/internal/__astro_component.ts
index 6738600e3..1ddd40b05 100644
--- a/packages/astro/src/internal/__astro_component.ts
+++ b/packages/astro/src/internal/__astro_component.ts
@@ -72,8 +72,10 @@ interface HydrateScriptOptions {
}
/** For hydrated components, generate a `;
diff --git a/packages/astro/test/astro-dynamic.test.js b/packages/astro/test/astro-dynamic.test.js
index 30bcf456c..9decd539c 100644
--- a/packages/astro/test/astro-dynamic.test.js
+++ b/packages/astro/test/astro-dynamic.test.js
@@ -26,6 +26,15 @@ DynamicComponents('Loads client-only packages', async ({ runtime }) => {
assert.equal(result.statusCode, 200, 'Can load react renderer');
});
+DynamicComponents('Loads pages using client:media hydrator', async ({ runtime }) => {
+ let result = await runtime.load('/media');
+ if (result.error) throw new Error(result.error);
+
+ let html = result.contents;
+ assert.ok(html.includes(`value: "(max-width: 700px)"`), 'static value rendered');
+ assert.ok(html.includes(`value: "(max-width: 600px)"`), 'dynamic value rendered');
+});
+
DynamicComponents('Can be built', async ({ build }) => {
try {
await build();
diff --git a/packages/astro/test/astro-markdown.test.js b/packages/astro/test/astro-markdown.test.js
index 73e17a5f8..3517c88f1 100644
--- a/packages/astro/test/astro-markdown.test.js
+++ b/packages/astro/test/astro-markdown.test.js
@@ -64,8 +64,6 @@ Markdown('Renders recursively', async ({ runtime }) => {
const result = await runtime.load('/recursive');
if (result.error) throw new Error(result.error);
- console.log(result.contents);
-
const $ = doc(result.contents);
assert.equal($('.a > h1').text(), 'A', 'Rendered title .a correctly');
assert.equal($('.b > h1').text(), 'B', 'Rendered title .b correctly');
diff --git a/packages/astro/test/fixtures/astro-dynamic/src/components/Counter.jsx b/packages/astro/test/fixtures/astro-dynamic/src/components/Counter.jsx
index 7a7d6ea1f..5c9d142e2 100644
--- a/packages/astro/test/fixtures/astro-dynamic/src/components/Counter.jsx
+++ b/packages/astro/test/fixtures/astro-dynamic/src/components/Counter.jsx
@@ -1,9 +1,11 @@
-import React from 'react';
+import React, { useState } from 'react';
export default function () {
+ const [count, setCount] = useState(0);
return (
-
+
Count: {count}
+
);
}
diff --git a/packages/astro/test/fixtures/astro-dynamic/src/pages/media.astro b/packages/astro/test/fixtures/astro-dynamic/src/pages/media.astro
new file mode 100644
index 000000000..3435f8df4
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-dynamic/src/pages/media.astro
@@ -0,0 +1,17 @@
+---
+import Counter from '../components/Counter.jsx';
+const MOBILE = "(max-width: 600px)";
+---
+
+
+ Media hydration
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file