vuejs / pinia

🍍 Intuitive, type safe, light and flexible Store for Vue using the composition api with DevTools support
https://pinia.vuejs.org
MIT License
12.72k stars 996 forks source link

`Set operation on key "value" failed` when returning `readonly` ref in SSR #2701

Closed enkot closed 3 weeks ago

enkot commented 3 weeks ago

Reproduction

https://stackblitz.com/edit/nuxt-starter-ytsmbt?file=store%2Ffoo.ts

Steps to reproduce the bug

Hi 👋 Start the project in dev mode.

Expected behavior

No warnings in the console.

Actual behavior

You can see in the console:

[Vue warn] Set operation on key "value" failed: target is readonly.

Additional information

I suggest skipping the readonly properties during hydration.

posva commented 3 weeks ago

This is expected. State must be returned and writable so it can be hydrated. The docs were not clear about this so I updated it

enkot commented 3 weeks ago

@posva But the question is, should readonly be hydrated at all? Shouldn't we treat them as computeds that are not hydrated?

enkot commented 3 weeks ago

Because currently we should do something like this when we need to mark something as readonly:

export const useAuthStore = defineStore('auth', () => {
  const foo = ref('foo')

  return {
    bar: skipHydrate(readonly(foo))
    // another option, but with redundant computed
    // bar: computed(() => foo.value)
  }
})
posva commented 3 weeks ago

State shouldn't be readonly. It must be writable. If you skip hydration but change it on the server, it will create a hydration mismatch.

enkot commented 3 weeks ago

Maybe I'm missing something 🙈, but readonly is always a proxy to some other reactive object (in some way similar to computed(() => other.value)), so why we skip computed during the hydration but can't skip readonly?

I think devtools can just mark them as readonly, because this works fine in SPA:

export const useFooStore = defineStore('foo', () => {
  const bar = ref(0);

  return {
    foo: readonly(bar),
    increment() {
      bar.value++;
    }
  };
});