A common use case for all data stores is a need to persist data. That way, if a user refreshes the page or begins a new session we can store (persist) the current state and use it later on when the user comes back.
While Vue’s state management library, Pinia, has a lot of great features, it does not automatically persist application state. So how do you persist Pinia state upon a browser refresh? In this tutorial, we’ll be exploring three different ways you can refresh-proof your Pinia stores.
Project Setup
To get started, if you don’t already have Pinia installed in your Vue app, add it now. In the root directory, run the following command to install Pinia:
yarn add pinia
# or with npm
npm install pinia
Then update your main.js
to use Pinia:
📄 main.js
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
app.mount("#app");
Create a stores
folder under src
and add a file called counter.js
with the following code:
📁src/stores/counter.js
import { defineStore } from "pinia";
export const useCounterStore = defineStore("counter", {
state: () => {
return {
count: 0
};
},
actions: {
increment(value = 1) {
this.count += value;
},
decrement(value = 1) {
this.count -= value;
},
reset() {
this.count = 0;
}
},
getters: {
doubleCount: (state) => {
return state.count * 2;
},
squareCount: (state) => {
return state.count ** 2;
},
},
});
We’ve created a state called count
with multiple actions and getters that can dictate our state’s behavior.
Let’s display this in our app to test that it works.
Update your App.vue
file to look like this:
📁src/App.vue
<script setup>
import { useCounterStore } from "./stores/counter"
const store = useCounterStore(); //initialize the store
</script>
<template>
<h1>Counter App</h1>
<p>Count is {{ store.count }}</p>
<p>Double count is {{ store.doubleCount }}</p>
<button @click="store.increment(1)">Add</button>
<button @click="store.decrement(1)">Subtract</button>
<button @click="store.reset">Reset</button>
</template>
<style scoped>
</style>
When we run the app, everything works as it should, but when we reload our page, the value of count
gets reset to its initial state: 0.
There is nothing broken here; Pinia does not by itself persist the state. If that’s the behavior we want, we have to solve for that ourselves. So let’s look at different ways we can solve this problem by persisting our store upon page reloads.
using Watch
The most common way to persist values is to save them in local storage and use the saved values to hydrate our Pinia store when the app loads.
We can use the watch
method from Vue to achieve this.
Update your main.js
to include the following code:
import { createApp, watch } from "vue";
...
watch(
pinia.state,
(state) => {
localStorage.setItem("counter", JSON.stringify(state.counter));
},
{ deep: true }
);
and update your counter.js
to include the following code:
state: () => {
if (localStorage.getItem("counter"))
return JSON.parse(localStorage.getItem("counter"));
return {
count: 0,
};
},
Let’s examine what our code is doing here. In the first code block, we use the watch
method to detect when there are changes to our state. If there’s a change, we want to update our localStorage with this change. In the second code block, after our store is created, we check if there’s already a value for our counter state in localStorage. If there is, we then set that value to our initial value for our counter
.
useLocalStorage from VueUse
The VueUse composable useLocalStorage provides a much simpler approach to persisting values in our store with much less code. useLocalstorage
will check our local storage for store values and will also smartly handle serialization for us based on the data type of provided default value.
To get started, install the VueUse library:
yarn add @vueuse/core
# or with npm
npm i @vueuse/core
Now we can import useLocalStorage
into our code and get persistence in just one line of code.
Update your counter store to include the following code:
📁src/stores/counter.js
import { useLocalStorage } from "@vueuse/core"
state: () => {
return {
count: useLocalStorage('count', 0), //useLocalStorage takes in a key of 'count' and default value of 0
};
},
Using a plugin
The final approach we’re going to look at is using a plugin. A great example is the Pinia plugin called persistedstate. This is a tiny package that is highly customizable, compatible with both Vue 2 and Vue 3, and (at the time of this writing) is being actively maintained.
To use this, we have to install it:
yarn add pinia-plugin-persistedstate
# or with npm
npm i pinia-plugin-persistedstate
Update your main.js
file to use the plugin:
📁src/main.js
...
import piniaPluginPersistedState from "pinia-plugin-persistedstate"
const pinia = createPinia();
pinia.use(piniaPluginPersistedState)
Finally, add a property called persist
and set it to true
in your counter store object.
📁src/stores/counter.js
export const useCounterStore = defineStore("counter", {
state: () => {
return {
count: 0,
};
},
...
persist: true,
});
You can also configure how a store is persisted by specifying options to the persist
property. For example, our plugin persists data in local storage by default but we can modify it to use session storage by updating the storage
configuration that exists in the persist
property:
export const useCounterStore = defineStore("counter", {
state: () => {
return {
count: 0,
};
},
...
persist: {
storage: sessionStorage, // data in sessionStorage is cleared when the page session ends.
},
});
All the available configuration options are explained here.
Where to go from here
Although we used a very simple example to demonstrate how to persist state in Pinia, the same approach would apply when working with more complex stores.
Please keep in mind that data stored in localStorage is not private and you should be careful with storing sensitive information there.
If you’re curious about Pinia, you should check out our Pinia Fundamentals course taught by Sandra Rogers.