vue.jsvuejs3vue-composition-apipiniavue-reactivity

Why is a computed property necessary for reactivity in a Vue component with Pinia store?


I am storing an authenticated user of my web app (Vue and Supabase) in a Pinia store: userStore.user. My NavBar component has a login/logout link based on this state. In order to make this reactive within my NavBar I need to make user a computed property: const user = computed(() => userStore.user).

Why does user in my NavBar need to be a computed property? I thought it would be reactive simply by pointing to user in userStore, since this is reactive within the Pinia store. Also, wrapping it in a ref did not make it reactive. I'm new to Vue and Pinia, and obviously missing some key concept.

Here's my code in context:

// UserStore.js

import { defineStore } from 'pinia'
import { supabase } from '@/supabase'

export const useUserStore = defineStore('user', {
  state: () => ({
    user: null,
    isLoggingIn: false,
    isLoggingOut: false
  }),
  actions: {
    async fetchUser() {
      const {
        data: { session }
      } = await supabase.auth.getSession()
      this.user = session?.user || null
    },
    async login(email, password) {
      this.isLoggingIn = true
      try {
        const { user, error } = await supabase.auth.signInWithPassword({ email, password })
        if (error) throw error
        this.user = user
      } finally {
        this.isLoggingIn = false
      }
    },
    async logout() {
      this.isLoggingOut = true
      try {
        await supabase.auth.signOut()
        this.user = null
      } finally {
        this.isLoggingOut = false
      }
    },
    setUser(user) {
      this.user = user
    }
  }
})
// NavBar.vue

<template>
  <nav>
    <router-link to="/">Home</router-link> |
    <router-link to="/restricted">Restricted</router-link> |
    <router-link v-if="!user" to="/login">Login</router-link>
    <button v-else @click="logout" :disabled="isLoggingOut">
      {{ isLoggingOut ? 'Logging out ...' : 'Logout' }}
    </button>
  </nav>
</template>

<script setup>
import { computed, ref } from 'vue'
import { useUserStore } from '@/stores/UserStore'

const userStore = useUserStore()

// const user = userStore.user  // Not reactive to changes in userStore - why?
// const user = ref(userStore.user)  // Also not reactive - why?
const user = computed(() => userStore.user) // This works

const isLoggingOut = computed(() => userStore.isLoggingOut)

const logout = userStore.logout
</script>

Solution

  • There is no way how it could maintain reactivity in JavaScript.

    This is what happens here:

    const foo = { bar: { baz: 'baz' } };
    const barCopy = foo.bar;
    foo.bar = { qux: 'qux' };
    console.log(barCopy) // baz: 'baz'

    This could work if existing user object were never reassigned but its properties were mutated instead, but in your case it's completely reassigned, which is a reasonable; it's not an object but null initially.

    In order to maintain reactivity, it should be consistently used as userStore.user in order to maintain the reference, this includes using it inside computed. Pinia's helper allows to create a writable computed in less verbose way:

    const { user } = storeToRefs(userStore);