From b094c72b3d40c52c7124f145a9db028509a11202 Mon Sep 17 00:00:00 2001 From: skirtle <65301168+skirtles-code@users.noreply.github.com> Date: Fri, 4 Oct 2024 09:09:23 +0100 Subject: [PATCH] fix(watch): watchEffect clean-up with SSR (#12097) close #11956 --- packages/runtime-core/src/apiWatch.ts | 16 +- .../__tests__/ssrWatch.spec.ts | 174 +++++++++++++++++- 2 files changed, 184 insertions(+), 6 deletions(-) diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 798b6e726..8f6168cdf 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -170,15 +170,14 @@ function doWatch( if (__DEV__) baseWatchOptions.onWarn = warn + // immediate watcher or watchEffect + const runsImmediately = (cb && immediate) || (!cb && flush !== 'post') let ssrCleanup: (() => void)[] | undefined if (__SSR__ && isInSSRComponentSetup) { if (flush === 'sync') { const ctx = useSSRContext()! ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = []) - } else if (!cb || immediate) { - // immediately watch or watchEffect - baseWatchOptions.once = true - } else { + } else if (!runsImmediately) { const watchStopHandle = () => {} watchStopHandle.stop = NOOP watchStopHandle.resume = NOOP @@ -226,7 +225,14 @@ function doWatch( const watchHandle = baseWatch(source, cb, baseWatchOptions) - if (__SSR__ && ssrCleanup) ssrCleanup.push(watchHandle) + if (__SSR__ && isInSSRComponentSetup) { + if (ssrCleanup) { + ssrCleanup.push(watchHandle) + } else if (runsImmediately) { + watchHandle() + } + } + return watchHandle } diff --git a/packages/server-renderer/__tests__/ssrWatch.spec.ts b/packages/server-renderer/__tests__/ssrWatch.spec.ts index c157c90cd..40c49740d 100644 --- a/packages/server-renderer/__tests__/ssrWatch.spec.ts +++ b/packages/server-renderer/__tests__/ssrWatch.spec.ts @@ -1,4 +1,12 @@ -import { createSSRApp, defineComponent, h, ref, watch } from 'vue' +import { + createSSRApp, + defineComponent, + h, + nextTick, + ref, + watch, + watchEffect, +} from 'vue' import { type SSRContext, renderToString } from '../src' describe('ssr: watch', () => { @@ -27,4 +35,168 @@ describe('ssr: watch', () => { expect(html).toMatch('hello world') }) + + test('should work with flush: sync and immediate: true', async () => { + const text = ref('start') + let msg = 'unchanged' + + const App = defineComponent(() => { + watch( + text, + () => { + msg = text.value + }, + { flush: 'sync', immediate: true }, + ) + expect(msg).toBe('start') + text.value = 'changed' + expect(msg).toBe('changed') + text.value = 'changed again' + expect(msg).toBe('changed again') + 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('changed again') + await nextTick() + expect(msg).toBe('changed again') + }) + + test('should run once with immediate: true', async () => { + const text = ref('start') + let msg = 'unchanged' + + const App = defineComponent(() => { + watch( + text, + () => { + msg = String(text.value) + }, + { immediate: true }, + ) + text.value = 'changed' + expect(msg).toBe('start') + return () => h('div', null, msg) + }) + + const app = createSSRApp(App) + const ctx: SSRContext = {} + const html = await renderToString(app, ctx) + + expect(ctx.__watcherHandles).toBeUndefined() + expect(html).toMatch('start') + await nextTick() + expect(msg).toBe('start') + }) + + test('should run once with immediate: true and flush: post', async () => { + const text = ref('start') + let msg = 'unchanged' + + const App = defineComponent(() => { + watch( + text, + () => { + msg = String(text.value) + }, + { immediate: true, flush: 'post' }, + ) + text.value = 'changed' + expect(msg).toBe('start') + return () => h('div', null, msg) + }) + + const app = createSSRApp(App) + const ctx: SSRContext = {} + const html = await renderToString(app, ctx) + + expect(ctx.__watcherHandles).toBeUndefined() + expect(html).toMatch('start') + await nextTick() + expect(msg).toBe('start') + }) +}) + +describe('ssr: watchEffect', () => { + test('should run with flush: sync', async () => { + const text = ref('start') + let msg = 'unchanged' + + const App = defineComponent(() => { + watchEffect( + () => { + msg = text.value + }, + { flush: 'sync' }, + ) + expect(msg).toBe('start') + text.value = 'changed' + expect(msg).toBe('changed') + text.value = 'changed again' + expect(msg).toBe('changed again') + 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('changed again') + await nextTick() + expect(msg).toBe('changed again') + }) + + test('should run once with default flush (pre)', async () => { + const text = ref('start') + let msg = 'unchanged' + + const App = defineComponent(() => { + watchEffect(() => { + msg = text.value + }) + text.value = 'changed' + expect(msg).toBe('start') + return () => h('div', null, msg) + }) + + const app = createSSRApp(App) + const ctx: SSRContext = {} + const html = await renderToString(app, ctx) + + expect(ctx.__watcherHandles).toBeUndefined() + expect(html).toMatch('start') + await nextTick() + expect(msg).toBe('start') + }) + + test('should not run for flush: post', async () => { + const text = ref('start') + let msg = 'unchanged' + + const App = defineComponent(() => { + watchEffect( + () => { + msg = text.value + }, + { flush: 'post' }, + ) + text.value = 'changed' + expect(msg).toBe('unchanged') + return () => h('div', null, msg) + }) + + const app = createSSRApp(App) + const ctx: SSRContext = {} + const html = await renderToString(app, ctx) + + expect(ctx.__watcherHandles).toBeUndefined() + expect(html).toMatch('unchanged') + await nextTick() + expect(msg).toBe('unchanged') + }) })