How to use Local Storage with Pinia

In this tutorial, we will explore multiple methods to persist our Pinia state inside LocalStorage. The use case for this can be to sync data that should be stored in the user’s browser, like an offline drawing application, or something similar. So how do we use LocalStorage inside a Pinia store?

There are multiple ways we can go about this. We can use the LocalStorage API directly, vue-use, or a persistence library made specifically for this purpose.

You can find the multiple approaches on GitHub. Let’s explore all of them by developing a simple Pinia store!

Example Pinia Store

We will have a small store, which will contain a settings object. It will have properties similar to the settings in VSCode.

import { defineStore } from 'pinia'

const STORE_NAME = 'main'

const getDefaultSettings = () => ({
  fontSize: 14,
  tabSize: 2,
  zoomLevel: 0,
})

export const useStore = defineStore(STORE_NAME, {
  state: () => ({
    settings: getDefaultSettings(),
  }),
  actions: {
    updateSettings(partialSettings) {
      this.settings = {
        ...this.settings,
        ...partialSettings,
      }
    },
  },
})

Furthermore, we have also an action that updates the settings using a partial object we pass as an argument. This will work with one level, if we want to update also the nested properties then we have to do some kind of a deep merge.

The app itself will be simple, with just one button to change the font size:

<script setup>
  import { storeToRefs } from 'pinia'
  import { useStore } from './store'
  import { computed } from 'vue'

  const store = useStore()

  const { settings } = storeToRefs(store)

  const handleToggleFontSizeClick = () => {
    store.updateSettings({
      fontSize: settings.value.fontSize === 14 ? 24 : 14,
    })
  }

  const fontSize = computed(() => `${settings.value.fontSize}px`)
</script>

<template>
  <div>
    <p :style="{ fontSize: fontSize }">the font size is {{ settings.fontSize }}</p>

    <button @click="handleToggleFontSizeClick">
      <template v-if="settings.fontSize === 14"> Switch to bigger font </template>

      <template v-else> Switch to smaller font </template>
    </button>
  </div>
</template>

The result should be like this:

pinia local storage example

Using LocalStorage

Now that we have our app running, let’s make it remember the font size we want to use. First, add Local Storage to the action and save the new settings value:

const SETTINGS_LOCAL_STORAGE_KEY = 'settings'

//...code

actions: {
  updateSettings(partialSettings) {
    this.settings = {
      ...this.settings,
      ...partialSettings,
    }

    localStorage.setItem(STORE_NAME, JSON.stringify(this.settings))
  },
},

Next, create a function that will either return the settings from Local Storage if it exists, otherwise, return the default settings:

const SETTINGS_LOCAL_STORAGE_KEY = 'settings'

const getDefaultSettings = () => ({
  fontSize: 14,
  tabSize: 2,
  zoomLevel: 0,
})

const getSettings = () => {
  const settings = localStorage.getItem(STORE_NAME)

  return settings ? JSON.parse(settings) : getDefaultSettings()
}

Finally, use the new getSettings function in the state:

state: () => ({
  settings: getSetting(),
}),

Now if we change the font size and reload we should see the persisted one used

pinia persist state in local storage

Using VueUse

With VueUse, we can use the useStorage composable. It won’t be much different than using Local Storage directly, however, it will save us a few lines of code.

Import the composable and use it instead of getSettings, pass the default settings as the second argument and you’re good to go:

import { defineStore } from 'pinia'
import { useStorage } from '@vueuse/core'

const STORE_NAME = 'main'

export const useStore = defineStore('main', {
  state: () => ({
    settings: useStorage(STORE_NAME, {
      fontSize: 14,
      tabSize: 2,
      zoomLevel: 0,
    }),
  }),
  actions: {
    updateSettings(partialSettings) {
      this.settings = {
        ...this.settings,
        ...partialSettings,
      }
    },
  },
})

As you can see, we don’t have to check if the settings are saved or not by default. we can just pass an initial value to useStorage and it will handle that logic internally.

It will also perform a shallow merge if the object stored in Local Storage doesn’t have properties of the initial value provided:

import { defineStore } from 'pinia'
import { useStorage } from '@vueuse/core'

const STORE_NAME = 'settings'

export const useStore = defineStore('main', {
  state: () => ({
    settings: useStorage(
      STORE_NAME,
      {
        fontSize: 14,
        tabSize: 2,
        zoomLevel: 0,
      },
      localStorage,
      {
        mergeDefaults: true, // <--- needed
      },
    ),
  }),
  actions: {
    updateSettings(partialSettings) {
      this.settings = {
        ...this.settings,
        ...partialSettings,
      }
    },
  },
})

We don’t have to “save” the new value anymore, since the composable will return a reactive object that can detect changes, vue use then will save the new changes automatically.

If we test again, we can see that it still works as expected.

Using a Vue Persistence Library

For more advanced applications that will store most of the state and rely heavily on Local Storage, a persistence library is more suitable. We won’t have to manually store properties separately.

The persistence library will basically save the whole state into Local Storage. You can of course reduce the state to only the properties you want to save.

we can just plug pinia-plugin-persiststate in Pinia and it will do its magic.

First, install the dependency:

npm i pinia-plugin-persistedstate

Then import and use the plugin before using Pinia in Vue:

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

createApp(App).use(pinia).mount('#app')

Finally, add the persist flag to the store:

import { defineStore } from 'pinia'

export const useStore = defineStore('main', {
  state: () => ({
    settings: {
      fontSize: 14,
      tabSize: 2,
      zoomLevel: 0,
    },
  }),
  actions: {
    updateSettings(partialSettings) {
      this.settings = {
        ...this.settings,
        ...partialSettings,
      }
    },
  },
  persist: true,
})

If you try it now, it should persist the store inside Local Storage using the store’s name as the key

pinia local storage persist state

Conclusion

After exploring the many approaches to persist state inside Local Storage, you can compare them and use the suitable one for your project:

  • Local Storage API: more work without using a library
  • VueUse: using the library, but shouldn’t be a problem since VueUse is tree-shakeable
  • Persistence library: for apps with bigger stores that rely heavily on Local Storage.
Amenallah Hsoumi
Amenallah Hsoumi

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

Articles: 19