Subscribe on changes!

Computed value not updated in SSR

avatar
Jan 20th 2022

Version

3.2.27

Reproduction link

github.com

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

avatar
Jan 21st 2022
this.effect.active = !isSSR

maybe this code make computed inactive in SSR env?

avatar
Nov 2nd 2023

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

avatar
Nov 2nd 2023

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)
})
avatar
Nov 27th 2023

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
      }
    }