Skip to content

Commit

Permalink
fix(watch): watchEffect clean-up with SSR (vuejs#12097)
Browse files Browse the repository at this point in the history
  • Loading branch information
skirtles-code authored Oct 4, 2024
1 parent 6e4de8d commit b094c72
Show file tree
Hide file tree
Showing 2 changed files with 184 additions and 6 deletions.
16 changes: 11 additions & 5 deletions packages/runtime-core/src/apiWatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
174 changes: 173 additions & 1 deletion packages/server-renderer/__tests__/ssrWatch.spec.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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')
})
})

0 comments on commit b094c72

Please sign in to comment.