diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 19026cf64..180843cf7 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -40,6 +40,8 @@ import { warn } from './warning' import { DeprecationTypes } from './compat/compatConfig' import { checkCompatEnabled, isCompatEnabled } from './compat/compatConfig' import { ObjectWatchOptionItem } from './componentOptions' +import { useSSRContext } from '@vue/runtime-core' +import { SSRContext } from '@vue/server-renderer' export type WatchEffect = (onCleanup: OnCleanup) => void @@ -280,7 +282,8 @@ function doWatch( } // in SSR there is no need to setup an actual effect, and it should be noop - // unless it's eager + // unless it's eager or sync flush + let ssrCleanup: (() => void)[] | undefined if (__SSR__ && isInSSRComponentSetup) { // we will also not call the invalidate callback (+ runner is not set up) onCleanup = NOOP @@ -293,7 +296,12 @@ function doWatch( onCleanup ]) } - return NOOP + if (flush === 'sync') { + const ctx = useSSRContext() as SSRContext + ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = []) + } else { + return NOOP + } } let oldValue: any = isMultiSource @@ -378,12 +386,15 @@ function doWatch( effect.run() } - return () => { + const unwatch = () => { effect.stop() if (instance && instance.scope) { remove(instance.scope.effects!, effect) } } + + if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch) + return unwatch } // this.$watch diff --git a/packages/server-renderer/__tests__/ssrWatch.spec.ts b/packages/server-renderer/__tests__/ssrWatch.spec.ts new file mode 100644 index 000000000..df085332d --- /dev/null +++ b/packages/server-renderer/__tests__/ssrWatch.spec.ts @@ -0,0 +1,30 @@ +import { createSSRApp, defineComponent, h, watch, ref } from 'vue' +import { SSRContext, renderToString } from '../src' + +describe('ssr: watch', () => { + // #6013 + test('should work w/ flush:sync', async () => { + const App = defineComponent(() => { + const count = ref(0) + let msg = '' + watch( + count, + () => { + msg = 'hello world' + }, + { flush: 'sync' } + ) + count.value = 1 + expect(msg).toBe('hello world') + return () => h('div', null, msg) + }) + + const app = createSSRApp(App) + const ctx: SSRContext = {} + const html = await renderToString(app, ctx) + + expect(ctx.__watcherHandles!.length).toBe(1) + + expect(html).toMatch('hello world') + }) +}) diff --git a/packages/server-renderer/src/render.ts b/packages/server-renderer/src/render.ts index 65123ac0c..a1f327b43 100644 --- a/packages/server-renderer/src/render.ts +++ b/packages/server-renderer/src/render.ts @@ -45,7 +45,14 @@ export type Props = Record export type SSRContext = { [key: string]: any teleports?: Record + /** + * @internal + */ __teleportBuffers?: Record + /** + * @internal + */ + __watcherHandles?: (() => void)[] } // Each component has a buffer array. diff --git a/packages/server-renderer/src/renderToStream.ts b/packages/server-renderer/src/renderToStream.ts index 79484f9cf..bdcdb0a02 100644 --- a/packages/server-renderer/src/renderToStream.ts +++ b/packages/server-renderer/src/renderToStream.ts @@ -76,6 +76,13 @@ export function renderToSimpleStream( Promise.resolve(renderComponentVNode(vnode)) .then(buffer => unrollBuffer(buffer, stream)) .then(() => resolveTeleports(context)) + .then(() => { + if (context.__watcherHandles) { + for (const unwatch of context.__watcherHandles) { + unwatch() + } + } + }) .then(() => stream.push(null)) .catch(error => { stream.destroy(error) diff --git a/packages/server-renderer/src/renderToString.ts b/packages/server-renderer/src/renderToString.ts index 3db4561c7..2fdc39f68 100644 --- a/packages/server-renderer/src/renderToString.ts +++ b/packages/server-renderer/src/renderToString.ts @@ -67,6 +67,12 @@ export async function renderToString( await resolveTeleports(context) + if (context.__watcherHandles) { + for (const unwatch of context.__watcherHandles) { + unwatch() + } + } + return result }