My Top 5 Tips for using Pinia

My Top 5 Tips for using Pinia

The Top 5 Tips for using Pinia by Eduardo San Martin Morote, the author of Pinia himself

Eduardo San Martin Morote

Eduardo San Martin Morote

September 13, 2023

I've had a lot of experience with Pinia, not just because I created it but because I often use it in my projects and review other people's code that use it! On top of that I often give talks about what I find useful to others. This gives me some unique insights and in this article I want to share my top 5 tips for using Pinia.

Here's the TL;DR:

  • Don't create useless getters
  • Use composables in Option Stores
  • Use Setup Stores for complex composables
  • Use Setup Stores to inject globals like the Router
  • How to create private State

Let's dive into each of these tips!

You don't need getters for everything

There was a common misconception in Vuex that you should always use getters to access state. This is not true. Getters are useful when you need to compute something from the state, for example, if you have a list of todos and you want to know how many are completed, you can create a getter for that. I can't tell how many times I've seen code like this in Tutorials, StackOverflow, or even in production code:

export default Vuex.Store({
  state: () => ({ counter: 0 }),
  getters: {
    // Completely useless getter
    getCount: state => state.counter,
  },
})

This was just unnecessary boilerplate in Vuex and it's still unnecessary in Pinia. You can just access the state directly:

const counterStore = useCounterStore()
counterStore.counter // 0 ✅

Extra Tip: most of the time you don't need storeToRefs() (or toRef()) either. You can just use the store directly! Vue reactivity is really convenient 😄.

Composables within Option Stores

You can use some composables within option stores, specifically, you can use any composable that holds state and is writable. For example, you can use useLocalStorage() from @vueuse/core to store some state in the browser's local storage.

import { useLocalStorage } from '@vueuse/core'
const useAuthStore = defineStore('auth', {
  state: () => ({
    user: useLocalStorage('pinia/user/login', 'alice'),
  }),
})

Or debounce changes to a ref with refDebounced():

import { refDebounced } from '@vueuse/core'
const useSearchStore = defineStore('search', {
  state: () => ({
    user: {
      text: refDebounced(/* ... */),
    },
  }),
})

Complex composables with Setup Stores

Within Setup stores you can use any composable you want. You can connect to a websocket, bluetooth handling or even gamepads!

import { useWebSocket } from '@vueuse/core'
export const useServerInfoStore = defineStore('server-info', () => {
  const { status, data, send, open, close } = useWebSocket('ws://websocketurl')
  return {
    status,
    data,
    send,
    open,
    close,
  }
})

Pinia will automatically identify what is state, getters, or actions. Remember that you must return all state properties from the setup function.

inject() within setup stores

You can use inject() within setup stores to access app-level provided variables like the router instance:

import { useRouter } from 'vue-router'
export const useAuthStore('auth', () => {
  const router = useRouter()
  function logout() {
    // logout the user
    return router.push('/login')
  }
  return {
    logout
  }
})

Private state with nested Stores

One of the golden rules of setup stores is to return every single piece of state:

export const useAuthStore('auth', () => {
  const user = ref<User | null>(null)
  const token = ref<string | null>(null)
  // we must return both user and token
  return {
    user,
    token,
  }
})

But what if we want to hide some state from a store? We can create a nested store that contains the private state instead:

export const usePrivateAuthState('auth-private', () => {
  const token = ref<string | null>(null)
  return {
    token,
  }
})
export const useAuthStore('auth', () => {
  const user = ref<User | null>(null)
  const privateState = usePrivateAuthState()
  privateState.token // accessible only within this store
  return {
    user,
  }
})

Bonus: Using client-only state with SSR

Server Side Rendering (SSR) is a great way to improve the performance of your application. However, it comes with some extra difficulties when compared to a client-only application. For example, you don't have access to window, or document, or any other browser-specific API like Local Storage.

In Option Stores, this requires you to use a hydrate option to tell Pinia that some state should not be hydrated on the client:

import { useLocalStorage } from '@vueuse/core'
const useAuthStore = defineStore('auth', {
  state: () => ({
    user: useLocalStorage('pinia/user/login', 'alice'),
  }),
  hydrate(state, initialState) {
    state.user = useLocalStorage('pinia/user/login', 'alice')
  },
})

On Setup Stores, you can use the skipHydrate helper to mark some state as client-only:

import { defineStore, skipHydrate } from 'pinia'
const useAuthStore = defineStore('auth', () => {
  const user = skipHydrate(useLocalStorage('pinia/user/login', 'alice'))
  return { user }
})

Conclusion

There are of course plenty of other tips that I could share but these are the ones that I find the most useful. Plus, most people don't know about them. Do you have any Pinia tips or tricks to share that you've found useful? If so, feel free to share in the comments below!

The Mastering Pinia Course is Here!

Get a free lesson delivered to your inbox, just click on the button below.

Buy Now