How to Watch Pinia State Inside Vue 3 Components

In this tutorial, we will explore multiple ways to watch Pinia store (state, and getters) inside a Vue component. Let’s get started!

Watch a Single State Property Change

I will demonstrate with a user auth state example. Let’s imagine we have a user store that stores a single-state property for simplicity, we can call it isLoggedIn. First, define the store:

import { defineStore } from 'pinia'

export const useUser = defineStore('user', {
  state: () => ({
    isLoggedIn: false,
  }),
  actions: {
    login() {
      this.isLoggedIn = true
    },
    logout() {
      this.isLoggedIn = false
    },
  },
})

Then, use it in the component:

<script setup>
  import { storeToRefs } from 'pinia'
  import { watch } from 'vue'
  import { useUser } from './store/user/user.store'

  const userStore = useUser()
  const { isLoggedIn } = storeToRefs(userStore)

</script>

<template>
  <button @click="userStore.login">Login</button>
  <button @click="userStore.logout">Logout</button>


  is logged in? {{ isLoggedIn }}
</template>

Now, if we click on the login button isLoggedIn will become true. Correspondingly, if we click on logout, isLoggedIn will become false.

Since we want to use the isLoggedIn property inside the template and have it reactive, we need to convert it into a ref. We use storeToRefs it for that.

To watch the property we can simply use the watch function from vue:

<script setup>
  import { storeToRefs } from 'pinia'
  import { watch } from 'vue'
  import { useUser } from './store/user/user.store'

  const userStore = useUser()
  const { isLoggedIn } = storeToRefs(userStore)

  watch(isLoggedIn, () => {
    console.log('isLoggedIn ref changed, do something!')
  })
</script>

<template>
  <button @click="userStore.login">Login</button>
  <button @click="userStore.logout">Logout</button>

  is logged in? {{ isLoggedIn }}
</template>

This works, however, there could be another use case for the store where we don’t want to use storeToRefs. For example, were only interested in watching and not rendering. Then we can use userStore.isLoggedIn directly.

However, if you try replacing isLoggedIn it won’t work. We need to wrap the value within an anonymous function:

<script setup>
  import { storeToRefs } from 'pinia'
  import { watch } from 'vue'
  import { useUser } from './store/user/user.store'

  const userStore = useUser()
  const { isLoggedIn } = storeToRefs(userStore)

  watch(isLoggedIn, () => {
    console.log('isLoggedIn ref changed, do something!')
  })

  watch(
    () => userStore.isLoggedIn,
    () => {
      console.log('isLoggedIn state changed, do something!')
    },
  )
</script>

<template>
  <button @click="userStore.login">Login</button>
  <button @click="userStore.logout">Logout</button>

  is logged in? {{ isLoggedIn }}
</template>

In general, this applies to any reactive source that is not a ref! more on that in the official docs.

Comparatively, when using something like VueUse whenever, we should follow the same pattern, since the composable uses watch in the background:

<script setup>
  import { storeToRefs } from 'pinia'
  import { useUser } from './store/user/user.store'
  import { whenever } from '@vueuse/core'

  const userStore = useUser()
  const { isLoggedIn } = storeToRefs(userStore)

  whenever(isLoggedIn, () => {
    console.log('logged in (ref), do something!')
  })

  whenever(
    () => userStore.isLoggedIn,
    () => {
      console.log('logged in, do something!')
    },
  )
</script>

<template>
  <button @click="userStore.login">Login</button>
  <button @click="userStore.logout">Logout</button>

  is logged in? {{ isLoggedIn }}
</template>

Watch Multiple State Property Changes

Earlier, we demonstrated watching a single property value change. However, we can also watch the whole store. In other words, when any property changes we get notified inside our component!

Using the $subscribe method, we can pass an anonymous function that will be executed every time a change happens:

<script setup>
  import { storeToRefs } from 'pinia'
  import { useUser } from './store/user/user.store'

  const userStore = useUser()
  const { isLoggedIn } = storeToRefs(userStore)

  userStore.$subscribe((mutation, state) => {
    console.log('a change happened')
    console.log(mutation, state)
  })
</script>

<template>
  <button @click="userStore.login">Login</button>
  <button @click="userStore.logout">Logout</button>

  is logged in? {{ isLoggedIn }}
</template>

If we trigger a change, we will see the following in the console:

pinia watch state $subscribe

As you can see, we get in the mutation object and events object which we can use to figure out which property changed, In addition, we get the fully updated state as a second parameter.

Furthermore, if we change multiple properties within our action, we will still get a single update notification.

Add another property to the state and mutate it in the login action:

import { defineStore } from 'pinia'

export const useUser = defineStore('user', {
  state: () => ({
    isLoggedIn: false,
    token: '',
  }),
  actions: {
    login() {
      this.isLoggedIn = true
      this.token = 'token'
    },
    logout() {
      this.isLoggedIn = false
    },
  },
})

and in the console you shall see:

pinia watch state $subscribe multiple state mutations

Watch Getters Value Changes

We can watch getters in a similar fashion to watching the state. We use an anonymous function when using the value directly, and we watch the value directly when it is a ref.

If we add in the store a getter:

import { defineStore } from 'pinia'

export const useUser = defineStore('user', {
  state: () => ({
    isLoggedIn: false,
    token: '',
  }),
  getters: {
    isLoggedInAndHasToken({ isLoggedIn, token }) {
      return isLoggedIn && token.length
    },
  },
  actions: {
    login() {
      this.isLoggedIn = true
      this.token = 'token'
    },
    logout() {
      this.isLoggedIn = false
    },
  },
})

then we can watch it in this manner:

<script setup>
  import { storeToRefs } from 'pinia'
  import { watch } from 'vue'
  import { useUser } from './store/user/user.store'

  const userStore = useUser()
  const { isLoggedIn, isLoggedInAndHasToken } = storeToRefs(userStore)

  watch(isLoggedInAndHasToken, () => {
    console.log('isLoggedInAndHasToken ref changed')
  })

  watch(
    () => userStore.isLoggedInAndHasToken,
    () => {
      console.log('isLoggedInAndHasToken changed')
    },
  )
</script>

<template>
  <button @click="userStore.login">Login</button>
  <button @click="userStore.logout">Logout</button>

  is logged in? {{ isLoggedIn }}
</template>

Conclusion

Thank you for reading! If you want to learn more about Pinia and Vue make sure to check the other tutorials.

Amenallah Hsoumi
Amenallah Hsoumi

Senior Software Engineer, Indie Hacker, building cool stuff on the web!

Articles: 19