Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(nuxt): use key to force server component re-rendering #19911

Merged
merged 14 commits into from Apr 20, 2023
Merged
45 changes: 40 additions & 5 deletions packages/nuxt/src/app/components/nuxt-island.ts
@@ -1,4 +1,5 @@
import { computed, createStaticVNode, defineComponent, getCurrentInstance, ref, watch } from 'vue'
import type { RendererNode } from 'vue'
import { computed, createStaticVNode, defineComponent, getCurrentInstance, h, ref, watch } from 'vue'
import { debounce } from 'perfect-debounce'
import { hash } from 'ohash'
import { appendHeader } from 'h3'
Expand Down Expand Up @@ -33,7 +34,7 @@ export default defineComponent({
const instance = getCurrentInstance()!
const event = useRequestEvent()

const html = ref<string>(process.client ? instance.vnode.el?.outerHTML ?? '<div></div>' : '<div></div>')
const html = ref<string>(process.client ? getFragmentHTML(instance?.vnode?.el).join('') ?? '<div></div>' : '<div></div>')
const cHead = ref<Record<'link' | 'style', Array<Record<string, string>>>>({ link: [], style: [] })
useHead(cHead)

Expand All @@ -51,7 +52,7 @@ export default defineComponent({
}
})
}

const key = ref(0)
async function fetchComponent () {
nuxtApp[pKey] = nuxtApp[pKey] || {}
if (!nuxtApp[pKey][hashId.value]) {
Expand All @@ -63,6 +64,7 @@ export default defineComponent({
cHead.value.link = res.head.link
cHead.value.style = res.head.style
html.value = res.html
key.value++
}

if (process.client) {
Expand All @@ -72,7 +74,40 @@ export default defineComponent({
if (process.server || !nuxtApp.isHydrating) {
await fetchComponent()
}

return () => createStaticVNode(html.value, 1)
return () => h((_, { slots }) => slots.default?.(), { key: key.value }, {
default: () => [createStaticVNode(html.value, 1)]
})
}
})

// TODO refactor with https://github.com/nuxt/nuxt/pull/19231
function getFragmentHTML (element: RendererNode | null) {
if (element) {
if (element.nodeName === '#comment' && element.nodeValue === '[') {
return getFragmentChildren(element)
}
return [element.outerHTML]
}
return []
}

function getFragmentChildren (element: RendererNode | null, blocks: string[] = []) {
if (element && element.nodeName) {
if (isEndFragment(element)) {
return blocks
} else if (!isStartFragment(element)) {
blocks.push(element.outerHTML)
}

getFragmentChildren(element.nextSibling, blocks)
}
return blocks
}

function isStartFragment (element: RendererNode) {
return element.nodeName === '#comment' && element.nodeValue === '['
}

function isEndFragment (element: RendererNode) {
return element.nodeName === '#comment' && element.nodeValue === ']'
}
10 changes: 7 additions & 3 deletions packages/nuxt/src/components/runtime/server-component.ts
@@ -1,4 +1,4 @@
import { computed, createStaticVNode, defineComponent, h, watch } from 'vue'
import { Fragment, computed, createStaticVNode, createVNode, defineComponent, h, ref, watch } from 'vue'
import { debounce } from 'perfect-debounce'
import { hash } from 'ohash'
import { appendHeader } from 'h3'
Expand Down Expand Up @@ -42,6 +42,7 @@ const NuxtServerComponent = defineComponent({
},
async setup (props) {
const nuxtApp = useNuxtApp()
const key = ref(0)
const hashId = computed(() => hash([props.name, props.props, props.context]))

const event = useRequestEvent()
Expand Down Expand Up @@ -92,11 +93,14 @@ const NuxtServerComponent = defineComponent({
useHead(() => res.data.value!.head)

if (process.client) {
watch(props, debounce(() => res.execute(), 100))
watch(props, debounce(async () => {
await res.execute()
key.value++
}, 100))
}

await res

return () => createStaticVNode(res.data.value!.html, 1)
return () => createVNode(Fragment, { key: key.value }, [createStaticVNode(res.data.value!.html, 1)])
}
})
23 changes: 20 additions & 3 deletions test/basic.test.ts
Expand Up @@ -337,6 +337,23 @@ describe('pages', () => {

await page.close()
})

it('/islands', async () => {
const page = await createPage('/islands')
await page.waitForLoadState('networkidle')
await page.locator('#increase-pure-component').click()
await page.waitForResponse(response => response.url().includes('/__nuxt_island/') && response.status() === 200)
await page.waitForLoadState('networkidle')
expect(await page.locator('.box').innerHTML()).toContain('"number": 101,')
await page.locator('#count-async-server-long-async').click()
await Promise.all([
page.waitForResponse(response => response.url().includes('/__nuxt_island/LongAsyncComponent') && response.status() === 200),
page.waitForResponse(response => response.url().includes('/__nuxt_island/AsyncServerComponent') && response.status() === 200)
])
await page.waitForLoadState('networkidle')
expect(await page.locator('#async-server-component-count').innerHTML()).toContain(('1'))
expect(await page.locator('#long-async-component-count').innerHTML()).toContain('1')
})
})

describe('rich payloads', () => {
Expand Down Expand Up @@ -1109,7 +1126,7 @@ describe('component islands', () => {
const result: NuxtIslandResponse = await $fetch('/__nuxt_island/RouteComponent?url=/foo')

if (isDev()) {
result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates'))
result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates') && (l.href.startsWith('_nuxt/components/islands/') && l.href.includes('_nuxt/components/islands/RouteComponent')))
}

expect(result).toMatchInlineSnapshot(`
Expand All @@ -1132,7 +1149,7 @@ describe('component islands', () => {
})
}))
if (isDev()) {
result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates'))
result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates') && (l.href.startsWith('_nuxt/components/islands/') && l.href.includes('_nuxt/components/islands/LongAsyncComponent')))
}
expect(result).toMatchInlineSnapshot(`
{
Expand All @@ -1153,7 +1170,7 @@ describe('component islands', () => {
})
}))
if (isDev()) {
result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates'))
result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates') && (l.href.startsWith('_nuxt/components/islands/') && l.href.includes('_nuxt/components/islands/AsyncServerComponent')))
}
expect(result).toMatchInlineSnapshot(`
{
Expand Down
6 changes: 3 additions & 3 deletions test/fixtures/basic/pages/islands.vue
Expand Up @@ -18,15 +18,15 @@ const count = ref(0)
<NuxtIsland name="PureComponent" :props="islandProps" />
<NuxtIsland name="PureComponent" :props="islandProps" />
</div>
<button @click="islandProps.number++">
<button id="increase-pure-component" @click="islandProps.number++">
Increase
</button>
<hr>
Route island component:
<div v-if="routeIslandVisible" class="box">
<NuxtIsland name="RouteComponent" :context="{ url: '/test' }" />
</div>
<button v-else @click="routeIslandVisible = true">
<button v-else id="show-route" @click="routeIslandVisible = true">
Show
</button>

Expand All @@ -35,7 +35,7 @@ const count = ref(0)
<div>
Async island component (20ms):
<NuxtIsland name="LongAsyncComponent" :props="{ count }" />
<button @click="count++">
<button id="count-async-server-long-async" @click="count++">
add +1 to count
</button>
</div>
Expand Down