Skip to content

Commit

Permalink
fix(nuxt): use key to force server component re-rendering (#19911)
Browse files Browse the repository at this point in the history
  • Loading branch information
huang-julien committed Apr 20, 2023
1 parent 8b7df05 commit e8e01ba
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 14 deletions.
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

0 comments on commit e8e01ba

Please sign in to comment.