diff --git a/docs/2.guide/2.directory-structure/1.components.md b/docs/2.guide/2.directory-structure/1.components.md
index 623bb6614f..d66cbdc842 100644
--- a/docs/2.guide/2.directory-structure/1.components.md
+++ b/docs/2.guide/2.directory-structure/1.components.md
@@ -165,6 +165,36 @@ If you would like the component to load after certain events occur, like a click
```
+If you would like to load the component when the window matches a media query, you can use the `LazyMedia` prefix:
+
+```vue [pages/index.vue]
+
+
+
+
+
+```
+
+For general purpose conditional hydration, you can use the `LazyIf` prefix:
+
+```vue [pages/index.vue]
+
+
+
+
+
+```
+
+If you would like to never hydrate a component, use the `LazyNever` prefix:
+
+```vue [pages/index.vue]
+
+
+
+
+
+```
+
### Custom hydration triggers
If you would like to override the default hydration triggers when dealing with delayed hydration, like changing the timeout, the options for the intersection observer, or the events to trigger the hydration, you can do so by supplying a `hydrate` prop to your lazy components.
@@ -172,9 +202,11 @@ If you would like to override the default hydration triggers when dealing with d
```vue [pages/index.vue]
-
-
-
+
+
+
+
+
```
@@ -188,6 +220,10 @@ If you would like to override the default hydration triggers when dealing with d
::read-more{to="/docs/api/utils/create-event-loader"}
::
+::read-more{to="https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia"}
+Read more about using `LazyMedia` components and the accepted values.
+::
+
::important
Nuxt will respect your component names, which means even if your components begin with a reserved prefix like Visible/Idle/Event they will not have delayed hydration. This is made to ensure you have full control over all your components and prevent breaking dynamic imports for those components. This also means you would need to explicitly add the prefix to those components. For example, if you have a component named `IdleBar`, you would need to use it like `` and not `` to make it a delayed hydration component, otherwise it would be treated as a regular [dynamic import](/docs/guide/directory-structure/components#dynamic-imports)
::
diff --git a/packages/nuxt/src/components/loader.ts b/packages/nuxt/src/components/loader.ts
index d7708af946..4fca3d1937 100644
--- a/packages/nuxt/src/components/loader.ts
+++ b/packages/nuxt/src/components/loader.ts
@@ -43,7 +43,7 @@ export const loaderPlugin = createUnplugin((options: LoaderOptions) => {
const s = new MagicString(code)
const nuxt = tryUseNuxt()
// replace `_resolveComponent("...")` to direct import
- s.replace(/(?<=[ (])_?resolveComponent\(\s*["'](lazy-|Lazy(?=[A-Z]))?(Idle|Visible|idle-|visible-|Event|event-)?([^'"]*)["'][^)]*\)/g, (full: string, lazy: string, modifier: string, name: string) => {
+ s.replace(/(?<=[ (])_?resolveComponent\(\s*["'](lazy-|Lazy(?=[A-Z]))?(Idle|Visible|idle-|visible-|Event|event-|Media|media-|If|if-|Never|never-)?([^'"]*)["'][^)]*\)/g, (full: string, lazy: string, modifier: string, name: string) => {
const normalComponent = findComponent(components, name, options.mode)
const modifierComponent = !normalComponent && modifier ? findComponent(components, modifier + name, options.mode) : null
const component = normalComponent || modifierComponent
@@ -75,31 +75,50 @@ export const loaderPlugin = createUnplugin((options: LoaderOptions) => {
}
if (lazy) {
+ const dynamicImport = `${genDynamicImport(component.filePath, { interopDefault: false })}.then(c => c.${component.export ?? 'default'} || c)`
if (modifier && normalComponent && nuxt?.options.experimental.delayedHydration === true) {
switch (modifier) {
case 'Visible':
case 'visible-':
imports.add(genImport(clientDelayedComponentRuntime, [{ name: 'createLazyIOComponent' }]))
identifier += '_delayedIO'
- imports.add(`const ${identifier} = createLazyIOComponent(${genDynamicImport(component.filePath, { interopDefault: false })}.then(c => c.${component.export ?? 'default'} || c))`)
+ imports.add(`const ${identifier} = createLazyIOComponent(${dynamicImport})`)
break
case 'Event':
case 'event-':
imports.add(genImport(clientDelayedComponentRuntime, [{ name: 'createLazyEventComponent' }]))
identifier += '_delayedEvent'
- imports.add(`const ${identifier} = createLazyEventComponent(${genDynamicImport(component.filePath, { interopDefault: false })}.then(c => c.${component.export ?? 'default'} || c))`)
+ imports.add(`const ${identifier} = createLazyEventComponent(${dynamicImport})`)
break
case 'Idle':
case 'idle-':
imports.add(genImport(clientDelayedComponentRuntime, [{ name: 'createLazyNetworkComponent' }]))
identifier += '_delayedNetwork'
- imports.add(`const ${identifier} = createLazyNetworkComponent(${genDynamicImport(component.filePath, { interopDefault: false })}.then(c => c.${component.export ?? 'default'} || c))`)
+ imports.add(`const ${identifier} = createLazyNetworkComponent(${dynamicImport})`)
+ break
+ case 'Media':
+ case 'media-':
+ imports.add(genImport(clientDelayedComponentRuntime, [{ name: 'createLazyMediaComponent' }]))
+ identifier += '_delayedMedia'
+ imports.add(`const ${identifier} = createLazyMediaComponent(${dynamicImport})`)
+ break
+ case 'If':
+ case 'if-':
+ imports.add(genImport(clientDelayedComponentRuntime, [{ name: 'createLazyIfComponent' }]))
+ identifier += '_delayedIf'
+ imports.add(`const ${identifier} = createLazyIfComponent(${dynamicImport})`)
+ break
+ case 'Never':
+ case 'never-':
+ imports.add(genImport('vue', [{ name: 'defineAsyncComponent', as: '__defineAsyncComponent' }]))
+ identifier += '_delayedNever'
+ imports.add(`const ${identifier} = __defineAsyncComponent({loader: ${dynamicImport}, hydrate: () => {})`)
break
}
} else {
imports.add(genImport('vue', [{ name: 'defineAsyncComponent', as: '__defineAsyncComponent' }]))
identifier += '_lazy'
- imports.add(`const ${identifier} = __defineAsyncComponent(${genDynamicImport(component.filePath, { interopDefault: false })}.then(c => c.${component.export ?? 'default'} || c)${isClientOnly ? '.then(c => createClientOnly(c))' : ''})`)
+ imports.add(`const ${identifier} = __defineAsyncComponent(${dynamicImport}${isClientOnly ? '.then(c => createClientOnly(c))' : ''})`)
}
} else {
imports.add(genImport(component.filePath, [{ name: component._raw ? 'default' : component.export, as: identifier }]))
diff --git a/packages/nuxt/src/components/runtime/client-delayed-component.ts b/packages/nuxt/src/components/runtime/client-delayed-component.ts
index 293533e714..fc64a7da82 100644
--- a/packages/nuxt/src/components/runtime/client-delayed-component.ts
+++ b/packages/nuxt/src/components/runtime/client-delayed-component.ts
@@ -1,4 +1,4 @@
-import { defineAsyncComponent, defineComponent, getCurrentInstance, h, hydrateOnIdle, hydrateOnInteraction, hydrateOnVisible } from 'vue'
+import { defineAsyncComponent, defineComponent, getCurrentInstance, h, hydrateOnIdle, hydrateOnInteraction, hydrateOnMediaQuery, hydrateOnVisible, ref, watch } from 'vue'
import type { AsyncComponentLoader, HydrationStrategy } from 'vue'
import { useNuxtApp } from '#app/nuxt'
@@ -64,3 +64,38 @@ export const createLazyEventComponent = (componentLoader: AsyncComponentLoader)
},
})
}
+
+/* @__NO_SIDE_EFFECTS__ */
+export const createLazyMediaComponent = (componentLoader: AsyncComponentLoader) => {
+ return defineComponent({
+ inheritAttrs: false,
+ setup (_, { attrs }) {
+ if (import.meta.server) {
+ return () => h(defineAsyncComponent(componentLoader), attrs)
+ }
+ return () => h(delayedHydrationComponent(componentLoader, hydrateOnMediaQuery(attrs.hydrate as string | undefined)))
+ },
+ })
+}
+
+/* @__NO_SIDE_EFFECTS__ */
+export const createLazyIfComponent = (componentLoader: AsyncComponentLoader) => {
+ return defineComponent({
+ inheritAttrs: false,
+ setup (_, { attrs }) {
+ if (import.meta.server) {
+ return () => h(defineAsyncComponent(componentLoader), attrs)
+ }
+ const shouldHydrate = ref(!!(attrs.hydrate ?? true))
+ if (shouldHydrate.value) {
+ return () => h(defineAsyncComponent(componentLoader), attrs)
+ }
+
+ const strategy: HydrationStrategy = (hydrate) => {
+ const unwatch = watch(shouldHydrate, () => hydrate(), { once: true })
+ return () => unwatch()
+ }
+ return () => h(delayedHydrationComponent(componentLoader, strategy))
+ },
+ })
+}
\ No newline at end of file
diff --git a/test/basic.test.ts b/test/basic.test.ts
index 958a09cc47..17d32c9be5 100644
--- a/test/basic.test.ts
+++ b/test/basic.test.ts
@@ -2697,21 +2697,27 @@ describe('lazy import components', () => {
expect(await page.locator('body').getByText('This shouldn\'t be visible at first with network!').all()).toHaveLength(1)
expect(await page.locator('body').getByText('This should be visible at first with viewport!').all()).toHaveLength(1)
expect(await page.locator('body').getByText('This should be visible at first with events!').all()).toHaveLength(2)
- const component = await page.locator('#lazyevent')
+ // The default value is immediately truthy
+ expect(await page.locator('body').getByText('This shouldn\'t be visible at first with conditions!').all()).toHaveLength(1)
+ const component = page.locator('#lazyevent')
const rect = (await component.boundingBox())!
await page.mouse.move(rect.x + rect.width / 2, rect.y + rect.height / 2)
await page.waitForLoadState('networkidle')
expect(await page.locator('body').getByText('This shouldn\'t be visible at first with events!').all()).toHaveLength(1)
+ await page.locator('#conditionbutton').click()
+ await page.waitForLoadState('networkidle')
+ expect(await page.locator('body').getByText('This shouldn\'t be visible at first with conditions!').all()).toHaveLength(2)
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight))
await page.waitForLoadState('networkidle')
expect(await page.locator('body').getByText('This shouldn\'t be visible at first with viewport!').all()).toHaveLength(1)
+ expect(await page.locator('body').getByText('This should always be visible!').all()).toHaveLength(1)
await page.close()
})
it('respects custom delayed hydration triggers and overrides defaults', async () => {
const { page } = await renderPage('/lazy-import-components')
await page.waitForLoadState('networkidle')
- const component = await page.locator('#lazyevent2')
+ const component = page.locator('#lazyevent2')
const rect = (await component.boundingBox())!
await page.mouse.move(rect.x + rect.width / 2, rect.y + rect.height / 2)
await page.waitForTimeout(500)
@@ -2723,9 +2729,9 @@ describe('lazy import components', () => {
expect(await page.locator('body').getByText('This shouldn\'t be visible at first with events!').all()).toHaveLength(1)
await page.close()
})
- it('does not delay hydration of components named after modifiers', async () => {
- const { page } = await renderPage('/lazy-import-components')
- await page.waitForFunction(() => window.useNuxtApp?.() && !window.useNuxtApp?.().isHydrating)
+ it('does not delay hydration of components named after modifiers', () => {
+ expect(html).toContain('This fake lazy event should be visible!')
+ expect(html).not.toContain('This fake lazy event shouldn\'t be visible!')
})
})
diff --git a/test/fixtures/basic/components/DelayedCondition.client.vue b/test/fixtures/basic/components/DelayedCondition.client.vue
new file mode 100644
index 0000000000..94996942bb
--- /dev/null
+++ b/test/fixtures/basic/components/DelayedCondition.client.vue
@@ -0,0 +1,5 @@
+
+
+ This shouldn't be visible at first with conditions!
+