Getting more out of your Pinia Stores  — Vue Amsterdam Conference 2022 Summary series — Second Talk
Tags: vue, pinia, javascript, webdev
Published: 2022-06-21 08:22:05

Welcome! Happy to see you in the second part of my Vuejs Amsterdam Conference 2022 summary series, in which I share a summary of all the talks with you.

You can read my JSWorld Conference 2022 Summary series (in four parts) [here](https://dev.to/mohsen_vaziri/series/18427), where I summarized all the talks of the first day. You can also find the [first Talk where Evan You talks about the State of Vue in 2022 here](https://dev.to/mohsen_vaziri/state-of-the-vuenion-vue-amsterdam-conference-2022-summary-series-first-talk-i6b).

## (Recurring) Introduction

After two and a half years, JSWorld and Vue Amsterdam Conference were back in Theater Amsterdam between 1 and 3 June, and I had the chance to attend this conference for the first time. I learned many things, met many wonderful people, spoke with great developers, and had a great time. On the first day the JSWorld Conference was held, and on the second and third days, the Vue Amsterdam.

The conference was full of information with great speakers, each of whom taught me something valuable. They all wanted to share their knowledge and information with other developers. So I thought it would be great if I could continue to share it and help others use it.

At first, I tried to share a few notes or slides, but I felt it was not good enough, at least not as good as what the speaker shared with me. so I decided to re-watch each speech, dive deeper into them, search, take notes and combine them with their slides and even their exact words in their speech and then share it with you so that what I share with you is at least at the same level as what I learned from them

## **A very important point**

**Everything you read during these few articles is the result of the effort and time of the speaker itself,** and I have only tried to learn them so that I can turn them into these articles. Even **many of the sentences written in these articles are exactly what they said or what they wrote in Slides.** This means if you learn something new, it is because of their efforts. (So if you see some misinformation blame them, not me, right? xD)

Last but not least, I may not dig into every technical detail or live codings in some of the speeches. But if you are interested and need more information, let me know and I’ll try to write a more detailed article separately. Also, don’t forget to check out their Twitter/Linkedin.

Here you can find the program of the conference:

[JSWORLD Conference](https://www.jsworldconference.com/program)

---

# Getting more out of your Pinia Stores

[Eduaro San Martin Morote](https://twitter.com/posva) - Creator at Vue-Router

Pinia might be a light library with a simple API but it takes advantage of many Vue Reactivity concepts like Effect Scopes, which are unknown by most. In this talk we will go through some of the internals of Pinia, understanding them and discovering how to enhance our usage of Pinia.

> During the time I was creating Pinia, I had a lot of experimenting going on with the `Refs` and `Reactives` and the whole reactivity system of Vue, which gave me a lot of insights on how to keep that one single source of truth that we love so much in Vue.****
> 

## State Alchemy with Pinia

Pinia is the successor of Vuex, without compromising all the developer experience that comes with Vuex. It is compatible with Vue 2 and Vue 3, and it’s lighter and Vuex.

One of the advantages of Pinia is that it’s Fully (automatically) type-safe. This library is based on the Composition API, but you don’t need to use the Composition API to use Pinia.

It has a testing library and a Nuxt module as well.

## Vuex vs Pinia

In Vuex you have some important rules:

- You can only change the state in mutations.
- You have to shout in action if you want to do it! (That’s why we write it in uppercase!)
- Mutations have to be synchronous and only actions can be asynchronous

```TypeScript
// Vuex

import { createApp } from 'vue'
import { createStore } from 'vuex'

// Create the store instance
const store = createStore({
	state: () => ({ count: 0 })
	mutations: {
		INCREMENT: state => { state.count++ }
	},
	getters: {
		double: state => state.count * 2
	},
	actions: {
		async increment ({ commit }) {
			await someAsyncCall()
			commit('increment')
		}
	}
})
// You have to use it as an Application plugin
createApp(App).use(store).mount('#app')
```

Now let’s take a look at Pinia:

```TypeScript
// Pinia

import { createApp } from 'vue'
import { defineStore, createPinia } from 'pinia'

const useStore = defineStore({
	state: () => ({ count: 0 })
	getters: {
		double: state => state.count * 2
	},
	actions: {
		async increment () {
			await someAsyncCall()
			// There is no mutation, you can access the state with this keyword
			this.state++
		}
	}
})

createApp(App).use(createPinia()).mount('#app')
```

There aren’t many changes, except you no longer have to shout to change the state, and there is a function that is not creating a store, it’s defining a store. The difference is, that function is going to return another function that we call to get the actual store. The reason is to handle all the different ways we use the store to have an out of the box dynamic module registrations. In the end, `CreatePinia()` the function creates the Pinia instance.****

## What does the Pinia instance really do?

If you look at the Pinia instance you’re going to notice two things.

A `use()` function which is for Pinia plugins.

A state, which is just a `ref` of an initially empty object which get’s populated after you call some stores.

That state is instantiated inside its own `EffectScope`. 

To explain `EffectScope` we can talk about components. When you have a component that is mounted, it’s going to create its own `EffectScope` , which is going to collect all the reactivity variables like watchers, computed, etc. under its umbrella, and when the component is unmounted, this `EffectScope` is disposed and all the variables go away.

It’s the same for Pinia, except it never disappears and is always there.

## Pinia Holds State

The stores are never going to hold the state by themselves, they're going to transfer that to the Pinia instance. So you can access the state at any time with `pinia.state.value`.

The first time you call the `useSomeStore()` in your application, it’s going to put the initial state into the store using the id of the store as the key.

```TypeScript
const authStore = useAuthStore()

// Here the id will be "auth"
pinia.state.value.auth = { /* ... */ }

// If we call another store, It will add another object
const cartStore = useCartStore()

pinia.state.value.cart = { /* ... */ }
```

So as the user navigates through the application, the store keeps growing as it needed.

```TypeScript
pinia.state.value = {
	auth: { /* ... */ },
	cart: { /* ... */ },
	// other stores' state
}
```

## Pinia connects state

You can access the whole state of a store through `$state`. This variable is just a getter to access the store in Pinia.

There are three different ways of accessing a state in the store.

- Through the Pinia instance: `pinia.state.value.auth.user.login`
- Through `$state` on the store instance: `authStore.$state.user.login`
- Directly through the store instance: `authStore.user.login`

Any of these variables is going to yield the same result, and also if you write to any of these variables you are also going to write to the three of them, in fact, you are only writing to the Pinia state, because **there is only one source of Truth.**

In Vue composition API there is a `toRef` function that gives you a `ref` to a reactive object. So if you have something that is reactive, you can get a `ref` to any part of the object and it will be connected and if one of them changes, the other one will change as well (and you have only one single source of truth).

When we define the store with `defineStore` :

```TypeScript
const useAuthStore = defineStore('auth', {
	state: () => ({
		user: {
			login: 'alice',
		},
	}),
}),

// pinia.state.value.auth
```

and when we instantiate the store with the `useAuthStore` function and pass an object to the state, effectively what is happening is that we are getting a `ref` out of the state.

```TypeScript
useAuthStore()

pinia.state.value.auth = {
	user: {
		login: 'alice',
	},
}

authStore.user = toRef(pinia.state.value.auth, 'user')
```

We don’t have to write `authStore.user.value.login`, we just read the property or write to it using `.value` which is called ref unwrapping.

`authStore` is a reactive object so any `ref` passed inside is going to get unwrapped.

## Ref Unwrapping

When you create a reactive object, any `ref` passed inside at any level of that object gets automatically unwrapped, so you don’t need `.value` anywhere and is gonna be type-safe.

<aside>
💡 You can avoid this behavior with `shallowRef()` and `shallowReactive()`

</aside>

```TypeScript
pinia.state = ref({}) // or reactive({})
pinia.state.value.auth = {
	user: {
		login: ref('alice'), // Don't do this! 🙅🏻‍♂️
	},
}
pinia.state.value.auth.user.login // alice
```

You should never write a `ref` inside a reactive object because there is no point in doing so, writing `ref(’alice’)` in the code above is the same as writing the string `‘alice’`.

But what is interesting is not refs themselves, but refs with all their behaviors attached to them.

### **Composables**

Composables are functions that have a stateful return. One of the common composables is `useLocalStorage` which is from a library called `VueUse`, and it gives you a `ref` that is going to read from and write to local storage.

```TypeScript
pinia.state = ref({}) // or reactive({})
pinia.state.value.auth = {
	user: {
		login: useLocalStorage('pinia/user/login', 'alice'),
	},
}
pinia.state.value.auth.user.login // alice
```

This means that we can call that composable inside of the state function when we define a store:

```TypeScript
import { useLocalStorage } from '@vueuse/core'

const useAuthStore = defineStore('auth', {
	state: () => ({
		user: {
			login: useLocalStorage('pinia/user/login', 'alice'),
		},
	}),
})
```

So if you have composables that return a writable source of truth (like a `ref`), you can put them inside state and they are going to get unwrapped. So the actual behavior that may be interesting for us becomes an implementation detail we don’t need to care about anymore because it is just going to work out of the box. For example `useColorMode` or `useRouteHash`.

But if you have a read-only state — which is more like a computed property — or you have a function — which is more like an action — it’s going to be a little different and we will see later how to use them.****

### When does this not work?

If you are doing CSR it works fine. However, if you are doing SSR or SSG there will be some problems.

The problem is the state function is only called once the first time we instantiate the store.

In CSR, we start with an empty state, then we call the store with `useAuthStore()` and that’s going to instantiate the state and become reactive, easy!

In SSR, we do the same, But the problem is when the page comes back to the client on the browser, although we have the Client Side Rendering again, but on the server, the state was already executed and it was already instantiated once and we are not going to do that again on the client. That means we don’t get an empty object here and we get the object from the server.

So when we call `useAuthStore`, `useLocalStorage` is no longer called, and we no longer have that composable creating all the watchers, connecting all the event listeners to the window, it becomes just a string and probably the server is going to give us `‘alice’` because there is no local storage.

<aside>
💡 Fortunately VueUse is going to make the local storage function work on the server as well.

</aside>

<aside>
💡 Some composables require a little bit more work if you want to make them work on SSR. For example, you have to check your current instance or check if there is a window object that you can rely on. 
If you want to learn more, you can check the source code of VueUse. It’s very comfortable because in the documentation, at the very bottom of every function you see a link to the source in the Github repository.

</aside>

But what do we need to make this work on SSR?

We need an extra function called `hydrate()` that is going to create the watchers and event listeners on the client-side. This function is called if the initial state exists at store creation.

```TypeScript
import { useLocalStorage } from '@vueuse/core'

const useAuthStore = defineStore('auth', {
	state: () => ({
		user: {
			login: useLocalStorage('pinia/user/login', 'alice'),
		},
	}),
	hydrate(state, initialState) {
		state.user.login = useLocalStorage('pinia/user/login', 'alice')
	},
})
```

So that state being set by `UseAuthStore` is going to be replaced by the hydrate function. To recap:

On SSR:

- We have an initial state:

```TypeScript
pinia.state.value: {}
```

- We call `useAuthStore()`

```TypeScript
useAuthStore()
```

- We have the new state:

```TypeScript
pinia.state.value: {
	auth: { /* ... */ },
}
```

and that is going to be sent to the client. Then on CSR:

- We start with that state

```TypeScript
pinia.state.value: {
	auth: { /* ... */ },
}
```

- We call `useAuthStore()`

```TypeScript
useAuthStore()
// does not call state()
```

- And then we have the `hydrate` function adding the little bit that we were missing from the state function again

```TypeScript
state.user.login = useLocalStorage('pinia/user/login', 'alice')
```

## Options Store vs Setup Store

Options store which is pretty much like options API is what we talked about so far.

Setup stores are stores that are defined with a function similar to the setup function in composition API.

```TypeScript
const useAuthStore = defineStore('auth', () => {
	const user = useLocalStorage(...)
	
	return { user }
})
```

So in this case we don’t have multiple properties like state, getters, and actions, we just have one function and it has to create reactive objects if you want a state and return them, and it’s going to have computed for getters and functions for actions.

```TypeScript
const useAuthStore = defineStore('auth', () => {
	const user = useLocalStorage(...)

	const isLoggedIn = computed(() => /* ... */)

	function login(user, password) { /* ... */ }
	function logout() { /* ... */ }
	
	return { user, login, logout, isLoggedIn }
})
```

In the end, you return anything you want to expose and you can keep anything you want to keep private.

Because we have just one function that defines everything, we can not just not call it! We have to call it both on the server and the client. We need to hint to Pinia that e.g. `useLocalStorage` shouldn’t be hydrated. 

```TypeScript
import { defineStore, skipHydrate } from 'pinia'

const useAuthStore = defineStore('auth', () => {
	const user = skipHydrate(useLocalStorage(...))

	const isLoggedIn = computed(() => /* ... */)

	function login(user, password) { /* ... */ }
	function logout() { /* ... */ }
	
	return { user, login, logout, isLoggedIn }
})
```

What happens behind the scene is that normally when we’re on the client after the server has rendered the application, the store goes through all the different properties that have been put into the initial state, takes the values from the initial state, and put them into the store state; more exactly into the ref that we return in the function: `user.value = pinia.state.value.auth.user // alice`

That ref created by the store is then transferred into the Pinia state so that we have one single source of truth: `pinia.state.value.auth.user = user // 1 source of truth`

The thing is with skipHydrate, we are skipping the first line, so the `user.value = …` that’s no longer happening.

We should not use `skipHydrate` everywhere. If we look at the `useEyeDropper` for example:

```TypeScript
import { defineStore, skipHydrate } from 'pinia'
import { useEyeDropper } from '@vueuse/core'

const useColorStore = defineStore('colors', () => {
	const { isSupported, open, sRGBHex } = useEyeDropper()
	// ...
	return {
		lastColor: sRGBHex, // Ref<string>
		open, // Function
		isSupported, // Boolean
	}
})
```

Among these three, only the `lastColor` is going to become a state.

What is interesting is that we can stack these refs together. So we can pass the sRGBHex

ref to the local storage.

```TypeScript
import { defineStore, skipHydrate } from 'pinia'
import { useEyeDropper, useLocalStorage } from '@vueuse/core'

const useColorStore = defineStore('colors', () => {
	const { isSupported, open, sRGBHex } = useEyeDropper()
	const lastColor = useLocalStorage('lastColor', sRGBHex)
	// ...
	return {
		lastColor, // Ref<string>
		open, // Function
		isSupported, // Boolean
	}
})
```

In this scenario, we want to use `skipHydrate`, because the `useLocalStorage` relies on what the browser is seeing and doesn’t care at all about the server response on the initial value for the `lastColor`. If we were using only the `useEyeDropper`, wouldn’t need the `skipHydrate` because most of the time we don’t care if we hydrate from the server-side response or not, because there is not a new value in the browser context that is interesting for us and that could get overwritten by what the server sent. But if we use the local storage then we need to make sure that the one in the browser prevails and the server one is just ignored.

So what you need is not to use `skipHydrate` on every single ref, just on the ones that you want to avoid setting the value from the initial state, and only on SSR of course.

## Summary

### Options Store

- Can use basic Composition API inside `state()`
- Can only return writable state
- Use `hydrate()` for composables

### Setup Store

- Use any Composable (like in `setup()`)
- Automatically tells apart state from actions from getters
- Use `skipHydrate()` to ignore values from the server

---

## End of the second Talk

I hope you enjoyed this part and it can be as valuable to you as it was to me.

You can find the next talk about Histoire [here](https://dev.to/mohsen_vaziri/fast-stories-powered-by-vite-histoire-vue-amsterdam-conference-2022-summary-series-third-talk-543n)