Skip to content

Commit b6191cb

Browse files
authored
fix(runtime-core): restore SSR setup state when handling async setup result (#15114)
fix #15113
1 parent 0516c43 commit b6191cb

2 files changed

Lines changed: 75 additions & 1 deletion

File tree

packages/runtime-core/src/component.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -894,7 +894,12 @@ function setupStatefulComponent(
894894
// return the promise so server-renderer can wait on it
895895
return setupResult
896896
.then((resolvedResult: unknown) => {
897-
handleSetupResult(instance, resolvedResult, isSSR)
897+
setInSSRSetupState(true)
898+
try {
899+
handleSetupResult(instance, resolvedResult, isSSR)
900+
} finally {
901+
setInSSRSetupState(false)
902+
}
898903
})
899904
.catch(e => {
900905
handleError(e, instance, ErrorCodes.SETUP_FUNCTION)

packages/server-renderer/__tests__/ssrWatch.spec.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,35 @@ describe('ssr: watch', () => {
130130
expect(msg).toBe('start')
131131
})
132132

133+
// #15113
134+
test('options-api watchers should not be created for async setup components', async () => {
135+
const open = ref(true)
136+
const spy = vi.fn()
137+
138+
const App = defineComponent({
139+
setup: () => Promise.resolve({}),
140+
computed: {
141+
isOpen() {
142+
return open.value
143+
},
144+
},
145+
watch: {
146+
isOpen: spy,
147+
},
148+
render() {
149+
return h('div', String(this.isOpen))
150+
},
151+
})
152+
153+
const html = await renderToString(createSSRApp(App))
154+
expect(html).toBe('<div>true</div>')
155+
156+
// mutating the state after the request is done should not trigger the watcher
157+
open.value = false
158+
await nextTick()
159+
expect(spy).not.toHaveBeenCalled()
160+
})
161+
133162
test('should not run non-immediate watchers registered after async context restore', async () => {
134163
const text = ref('start')
135164
let beforeAwaitTriggered = false
@@ -351,4 +380,44 @@ describe.skipIf(!global.gc)('ssr: watch gc', () => {
351380

352381
expect(weakRefs.filter(ref => ref.deref()).length).toBe(0)
353382
})
383+
384+
// #15113
385+
test('options-api should not retain apps with async setup + watcher on global state', async () => {
386+
const weakRefs: { deref(): unknown | undefined }[] = []
387+
388+
const open = ref(true)
389+
390+
const App = defineComponent({
391+
setup: () => Promise.resolve({}),
392+
computed: {
393+
isOpen() {
394+
return open.value
395+
},
396+
},
397+
watch: {
398+
isOpen() {},
399+
},
400+
render() {
401+
return h('div', String(this.isOpen))
402+
},
403+
})
404+
405+
async function renderOnce() {
406+
const app = createSSRApp(App)
407+
// @ts-expect-error ES2021 API
408+
weakRefs.push(new WeakRef(app))
409+
const html = await renderToString(app)
410+
expect(html).toBe('<div>true</div>')
411+
}
412+
413+
for (let i = 0; i < 10; i++) {
414+
await renderOnce()
415+
}
416+
417+
for (let i = 0; i < 5; i++) {
418+
await gc()
419+
}
420+
421+
expect(weakRefs.filter(ref => ref.deref()).length).toBe(0)
422+
})
354423
})

0 commit comments

Comments
 (0)