diff --git a/docs/3.api/1.components/8.nuxt-island.md b/docs/3.api/1.components/8.nuxt-island.md index cd715534f2..2ede46ad88 100644 --- a/docs/3.api/1.components/8.nuxt-island.md +++ b/docs/3.api/1.components/8.nuxt-island.md @@ -32,7 +32,10 @@ Server only components use `` under the hood - **type**: `Record` - `source`: Remote source to call the island to render. - **type**: `string` -- **dangerouslyLoadClientComponents**: Required to load components from a remote source. +- `useCache`: Use the cached payload if available + - **type**: `boolean` + - **default**: `true` +- **`dangerouslyLoadClientComponents`**: Required to load components from a remote source. - **type**: `boolean` - **default**: `false` diff --git a/packages/nuxt/src/app/components/nuxt-island.ts b/packages/nuxt/src/app/components/nuxt-island.ts index c6bb6d8657..2384785ab8 100644 --- a/packages/nuxt/src/app/components/nuxt-island.ts +++ b/packages/nuxt/src/app/components/nuxt-island.ts @@ -75,6 +75,14 @@ export default defineComponent({ type: Boolean, default: false, }, + /** + * use the NuxtIslandResponse which has been cached if available + * @default true + */ + useCache: { + type: Boolean, + default: true, + }, }, emits: ['error'], async setup (props, { slots, expose, emit }) { @@ -239,13 +247,13 @@ export default defineComponent({ } if (import.meta.client) { - watch(props, debounce(() => fetchComponent(), 100), { deep: true }) + watch(props, debounce(() => fetchComponent(!props.useCache), 100), { deep: true }) } if (import.meta.client && !instance.vnode.el && props.lazy) { - fetchComponent() + fetchComponent(!instance.vnode.el && !props.useCache) } else if (import.meta.server || !instance.vnode.el || !nuxtApp.payload.serverRendered) { - await fetchComponent() + await fetchComponent(!instance.vnode.el && !props.useCache) } else if (selectiveClient && canLoadClientComponent.value) { await loadComponents(props.source, payloads.components) } diff --git a/packages/nuxt/src/components/runtime/server-component.ts b/packages/nuxt/src/components/runtime/server-component.ts index c5ecee9b2e..6dda4dd241 100644 --- a/packages/nuxt/src/components/runtime/server-component.ts +++ b/packages/nuxt/src/components/runtime/server-component.ts @@ -8,7 +8,7 @@ export const createServerComponent = (name: string) => { return defineComponent({ name, inheritAttrs: false, - props: { lazy: Boolean }, + props: { lazy: Boolean, useCache: { type: Boolean, default: true } }, emits: ['error'], setup (props, { attrs, slots, expose, emit }) { const vm = getCurrentInstance() @@ -22,6 +22,7 @@ export const createServerComponent = (name: string) => { return h(NuxtIsland, { name, lazy: props.lazy, + useCache: props.useCache, props: attrs, scopeId: vm?.vnode.scopeId, ref: islandRef, @@ -40,7 +41,7 @@ export const createIslandPage = (name: string) => { name, inheritAttrs: false, props: { lazy: Boolean }, - async setup (props, { slots, expose }) { + async setup(props, { slots, expose }) { const islandRef = ref(null) expose({ diff --git a/test/nuxt/nuxt-island.test.ts b/test/nuxt/nuxt-island.test.ts index a12123884f..3d761567f8 100644 --- a/test/nuxt/nuxt-island.test.ts +++ b/test/nuxt/nuxt-island.test.ts @@ -1,5 +1,4 @@ -import { beforeEach } from 'node:test' -import { describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { defineComponent, h, nextTick, popScopeId, pushScopeId } from 'vue' import { mountSuspended } from '@nuxt/test-utils/runtime' import { createServerComponent } from '../../packages/nuxt/src/components/runtime/server-component' @@ -34,6 +33,11 @@ function expectNoConsoleIssue () { beforeEach(() => { consoleError.mockClear() consoleWarn.mockClear() + useNuxtApp().payload.data = {} +}) + +afterEach(() => { + if (vi.isMockFunction(fetch)) { vi.mocked(fetch).mockReset() } }) describe('runtime server component', () => { @@ -135,6 +139,22 @@ describe('runtime server component', () => { }) it('expect NuxtIsland to have parent scopeId', async () => { + const stubFetch = vi.fn(() => { + return { + id: '1234', + html: `
hello
`, + head: { + link: [], + style: [], + }, + json () { + return this + }, + } + }) + + vi.stubGlobal('fetch', stubFetch) + const wrapper = await mountSuspended(defineComponent({ render () { pushScopeId('data-v-654e2b21') @@ -330,6 +350,63 @@ describe('client components', () => { " `) + vi.mocked(fetch).mockReset() expectNoConsoleIssue() }) }) + +describe('reuse paylaod', () => { + let count = 0 + + const stubFetch = () => { + count++ + return { + id: '123', + html: `
${count.toString()}
`, + state: {}, + head: { + link: [], + style: [], + }, + json () { + return this + }, + } + } + + beforeEach(() => { + count = 0 + vi.mocked(fetch).mockReset() + vi.stubGlobal('fetch', vi.fn(stubFetch)) + }) + it('expect payload to be reused', async () => { + const component1 = await mountSuspended(createServerComponent('reuseCache')) + expect(fetch).toHaveBeenCalledOnce() + expect(component1.html()).toBe('
1
') + await component1.unmount() + expect(fetch).toHaveBeenCalledOnce() + const component2 = await mountSuspended(createServerComponent('reuseCache')) + expect(component2.html()).toBe('
1
') + }) + it('expect to re-fetch the island', async () => { + const component = await mountSuspended(createServerComponent('withoutCache'), { + props: { + useCache: false, + onError (e) { + console.log(e) + }, + }, + }) + await nextTick() + expect(fetch).toHaveBeenCalledOnce() + expect(component.html()).toBe('
1
') + await component.unmount() + const component2 = await mountSuspended(createServerComponent('withoutCache'), { + props: { + useCache: false, + }, + }) + expect(fetch).toHaveBeenCalledTimes(2) + expect(component2.html()).toBe('
2
') + }) +})