Computed value not updated in SSR
Version
3.2.27
Reproduction link
Steps to reproduce
yarn
node index.js
What is expected?
Computed value is updated and html renders with correct message.
<div>hello world</div>
What is actually happening?
Computed value is not updated and an empty div is rendered.
<div></div>
I'm not sure if this a bug or is it me misusing computed in SSR context, but such an app architecture as in my reproduction demo seems to me completely valid and should work.
There is a computed value in a component which is based on part of app's state. No value serves as indication of the need to fetch data. Normally this should happen either during SSR or on the client if the state (or its part) was not hydrated. Async setup function allows to wait for data to be fetched which should trigger an update of state and computed value.
This acually worked in 3.2.26. New behavior was possibly introduced in commit f4f0966
I'm reopening this as it is still not fixed (the same reproduction still fails). It looks like it got fixed because of the line that reads msg.value
after fetching:
But removing that line from the test makes it fail. I think that that line should just be removed from the test anyway as the real assertion should happen on the rendered HTML.
It would be worth adding this test case too:
test('computed reactivity during SSR with onServerPrefetch', async () => {
const store = {
// initial state could be hydrated
state: reactive({ items: null as null | string[] }),
// pretend to fetch some data from an api
async fetchData() {
this.state.items = ['hello', 'world']
}
}
const getterSpy = vi.fn()
const App = defineComponent(() => {
const msg = computed(() => {
getterSpy()
return store.state.items?.join(' ')
})
// If msg value is falsy then we are either in ssr context or on the client
// and the initial state was not modified/hydrated.
// In both cases we need to fetch data.
onServerPrefetch(() => store.fetchData())
// simulate the read from a composable (e.g. filtering a list of results)
msg.value
return () => h('div', null, msg.value)
})
const app = createSSRApp(App)
// in real world serve this html and append store state for hydration on client
const html = await renderToString(app)
expect(html).toMatch('hello world')
// should only be called twice since access should be cached
// during the render phase
expect(getterSpy).toHaveBeenCalledTimes(2)
})
it uses onServerPrefetch()
instead of an async component
BTW a workaround is to read again the computed once it has been fetched:
const App = defineComponent(() => {
const msg = computed(() => store.state.items?.join(' '))
// If msg value is falsy then we are either in ssr context or on the client
// and the initial state was not modified/hydrated.
// In both cases we need to fetch data.
onServerPrefetch(async () => {
await store.fetchData()
msg.value
})
// simulate the read from a composable (e.g. filtering a list of results)
msg.value
return () => h('div', null, msg.value)
})
or with await
const App = defineComponent(async () => {
const msg = computed(() => store.state.items?.join(' '))
// simulate the read from a composable (e.g. filtering a list of results)
msg.value
// If msg value is falsy then we are either in ssr context or on the client
// and the initial state was not modified/hydrated.
// In both cases we need to fetch data.
await store.fetchData()
msg.value
return () => h('div', null, msg.value)
})
The root cause is that computed getters are cached before rendering: https://github.com/vuejs/core/blob/2f91872e7b64fb5e497529a9ed21c76c602bad2c/packages/server-renderer/src/render.ts#L131-L136
If we had to do this, we should make the computed dirty before caching
for (const e of instance.scope.effects) {
if (e.computed){
e.computed._dirty = true
e.computed._cacheable = true
}
}