]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(watch): watchEffect clean-up with SSR (#12097)
authorskirtle <65301168+skirtles-code@users.noreply.github.com>
Fri, 4 Oct 2024 08:09:23 +0000 (09:09 +0100)
committerGitHub <noreply@github.com>
Fri, 4 Oct 2024 08:09:23 +0000 (16:09 +0800)
close #11956

packages/runtime-core/src/apiWatch.ts
packages/server-renderer/__tests__/ssrWatch.spec.ts

index 798b6e7261b7ac9205c8e96696065abeb4560a8c..8f6168cdf299f03bc44bfcd5b65b40d8a1e04212 100644 (file)
@@ -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
 }
 
index c157c90cd42eb7a27fc06c1a83f3491516dd7bf1..40c49740d3e16a2210d1076e691c9f3847229cdb 100644 (file)
@@ -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')
+  })
 })