---
title: "Keep state across reloads with useStorage() · vuejs.school"
description: "useStorage() from VueUse gives you a ref that saves itself to localStorage. Build a settings panel that survives a reload, and avoid the one default-value trap."
canonical_url: "https://vuejs.school/community/blog/keep-state-across-reloads-with-usestorage"
meta:
  "og:description": "useStorage() from VueUse gives you a ref that saves itself to localStorage. Build a settings panel that survives a reload, and avoid the one default-value trap."
  "og:image": "https://vuejs.school/_og/d/a_~S2VlcCBzdGF0ZSBhY3Jvc3MgcmVsb2FkcyB3aXRoIHVzZVN0b3JhZ2UoKSDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_Keep%2Bstate%2Bacross%2Breloads%2Bwith%2BuseStorage()%2Cdescription_~dXNlU3RvcmFnZSgpIGZyb20gVnVlVXNlIGdpdmVzIHlvdSBhIHJlZiB0aGF0IHNhdmVzIGl0c2VsZiB0byBsb2NhbFN0b3JhZ2UuIEJ1aWxkIGEgc2V0dGluZ3MgcGFuZWwgdGhhdCBzdXJ2aXZlcyBhIHJlbG9hZCwgYW5kIGF2b2lkIHRoZSBvbmUgZGVmYXVsdC12YWx1ZSB0cmFwLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_sand%2Cp_Ii9jb21tdW5pdHkvYmxvZy9rZWVwLXN0YXRlLWFjcm9zcy1yZWxvYWRzLXdpdGgtdXNlc3RvcmFnZSI%2Cs_3OlvNzamu7dI4eO_.png"
  "og:image:alt": "Keep state across reloads with useStorage() · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Keep state across reloads with useStorage() · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~S2VlcCBzdGF0ZSBhY3Jvc3MgcmVsb2FkcyB3aXRoIHVzZVN0b3JhZ2UoKSDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_Keep%2Bstate%2Bacross%2Breloads%2Bwith%2BuseStorage()%2Cdescription_~dXNlU3RvcmFnZSgpIGZyb20gVnVlVXNlIGdpdmVzIHlvdSBhIHJlZiB0aGF0IHNhdmVzIGl0c2VsZiB0byBsb2NhbFN0b3JhZ2UuIEJ1aWxkIGEgc2V0dGluZ3MgcGFuZWwgdGhhdCBzdXJ2aXZlcyBhIHJlbG9hZCwgYW5kIGF2b2lkIHRoZSBvbmUgZGVmYXVsdC12YWx1ZSB0cmFwLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_sand%2Cp_Ii9jb21tdW5pdHkvYmxvZy9rZWVwLXN0YXRlLWFjcm9zcy1yZWxvYWRzLXdpdGgtdXNlc3RvcmFnZSI%2Cs_3OlvNzamu7dI4eO_.png"
  "twitter:image:alt": "Keep state across reloads with useStorage() · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## Why a ref forgets on reload

Here is a theme switch the way most people first write it:

*components/ThemeSwitch.vue*

```
<script setup lang="ts">
import { ref } from 'vue'

const theme = ref<'light' | 'dark'>('light')
</script>

<template>
  <button @click="theme = theme === 'light' ? 'dark' : 'light'">
    Theme: {{ theme }}
  </button>
</template>
```

It works until the user presses reload. A ref lives in memory, and memory is cleared with the page.

To keep a value, the browser offers `localStorage`: a small key-value store, strings only, that belongs to your site and keeps its data when the browser closes. Wiring a ref to it by hand means reading the key on start, parsing it, watching the ref, and writing on every change. `useStorage()` is that wiring, done once and tested.

## Replace the ref with useStorage()

Install VueUse if the project does not have it yet. Version 12.7 is current:

```
npm install @vueuse/core
```

Then replace the `ref` with `useStorage`. The first argument is the storage key, the second the value to use when nothing is stored yet:

*components/ThemeSwitch.vue*

```
<script setup lang="ts">
import { useStorage } from '@vueuse/core'

const theme = useStorage<'light' | 'dark'>('theme', 'light')
</script>

<template>
  <button @click="theme = theme === 'light' ? 'dark' : 'light'">
    Theme: {{ theme }}
  </button>
</template>
```

Nothing else changes. `theme` is still a ref. You read `theme.value` in the script and `theme` in the template, and you assign to it as before.

Behind the scenes, `useStorage` reads `localStorage.getItem('theme')` when the component is set up:

- If the key exists, the ref starts with the stored value.
- If it does not, the ref starts with `'light'`, and that default is written to storage straight away.
- From then on, every change to the ref is written back.

The serialiser (the code that turns a value into a string and back) is picked from the type of the default. A string default is stored as is, a number is written as text and parsed back into a number, a boolean becomes `'true'` or `'false'`, and an object or array goes through JSON. `Date`, `Map` and `Set` are handled too. For anything else, the [useStorage() docs](https://vueuse.org/core/useStorage/) show how to pass your own `serializer`.

## Store a whole settings object

A settings panel usually has more than one value. Pass an object as the default and `useStorage` stores the whole thing as JSON under one key. Changes to a nested property are saved too, because the ref is watched deeply by default.

*components/SettingsPanel.vue*

```
<script setup lang="ts">
import { useStorage } from '@vueuse/core'

const settings = useStorage('settings', {
  theme: 'light' as 'light' | 'dark',
  fontSize: 16,
})
</script>

<template>
  <section :class="settings.theme" :style="{ fontSize: `${settings.fontSize}px` }">
    <label>
      Theme
      <select v-model="settings.theme">
        <option value="light">Light</option>
        <option value="dark">Dark</option>
      </select>
    </label>
    <label>
      Font size: {{ settings.fontSize }}px
      <input v-model.number="settings.fontSize" type="range" min="12" max="24" />
    </label>
    <p>This text follows your settings, and will again after a reload.</p>
  </section>
</template>
```

Change the font size, reload, and the slider is where you left it. Two lines are worth explaining:

- `v-model.number` on the range input turns its value into a number. Without it the input hands you the string `'18'`, which is saved as a string while TypeScript still calls it a number, so `settings.fontSize + 2` gives `'182'`.
- TypeScript infers the type of `settings` from the default, which is why `theme` is written with `as 'light' | 'dark'`. Without it, `theme` would accept any string.

## Why a changed default does not reach existing users

Suppose the panel has shipped with `fontSize: 16` and you decide the default should be 18. You change the code, reload, and still see 16. Nothing is broken: your browser has a `settings` key from the earlier version, and a stored value always beats the default.

New users get 18. Everyone who opened the page before gets whatever they had, including people who never touched the slider, because `useStorage` wrote the old default on their first visit.

In production, treat a stored default as the user's choice, because to them it is. If you genuinely need to reset a user's value, assign `null` to the ref: `settings.value = null` removes the key, and the next page load starts from the default again.

## Add a new setting with mergeDefaults

The same rule catches you when the object grows. Three months later you add a `density` option. Users who already have a `settings` key load `{ theme, fontSize }` without `density`, and `settings.value.density` comes back `undefined`.

The `mergeDefaults` option fixes this. With `true`, the default and the stored object are merged shallowly, so new top-level keys get their default and existing ones keep the stored value.

Options are the fourth argument of `useStorage()`, after the storage object. `useLocalStorage()` is `useStorage()` with `localStorage` already chosen, so its options come third. The panel's call becomes:

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

const settings = useLocalStorage(
  'settings',
  { theme: 'light', fontSize: 16, density: 'comfortable' },
  { mergeDefaults: true },
)
```

Use `useLocalStorage()` whenever you need options. Passing `localStorage` to `useStorage()` by hand breaks on a server, where that name does not exist.

A shallow merge does not reach inside nested objects. For those, pass a function instead of `true` and merge however you like: `mergeDefaults: (stored, defaults) => ({ ...defaults, ...stored })` is the shallow version written out, and you can go deeper from there. Turn `mergeDefaults` on for any object you expect to grow.

## Session storage and syncing between tabs

`useSessionStorage()` has the same signature as `useLocalStorage()` and uses `sessionStorage`, which is cleared when the tab closes. It is the right choice for a draft of a form or a wizard step that should not come back tomorrow:

```
import { useSessionStorage } from '@vueuse/core'

const draft = useSessionStorage('contact-draft', { name: '', message: '' })
```

With `localStorage`, the browser fires a `storage` event in every other open tab of your site when a value changes, and `useStorage` listens for it. Change the theme in one tab and the other tabs follow without a reload. That is on by default; set `listenToStorageChanges: false` if two tabs should be allowed to disagree.

## Avoid hydration mismatches with initOnMounted

On the server there is no `localStorage`, so during server-side rendering `useStorage` returns a ref holding the default, and the HTML is rendered with it. In the browser, by default, the stored value is read during setup, before hydration (Vue taking over the server's HTML) has finished. If the user had chosen dark, the server sent light and the client immediately wants dark.

Vue reports this as a hydration mismatch, and it only partly repairs it. Text is updated to the client's value, but attributes such as `class` and `style` keep what the server sent. A user who saved dark and 20px in the settings panel sees the label "Font size: 20px", but the text is still 16px and the theme still light.

The `initOnMounted` option moves the read to after the component is mounted:

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

const theme = useLocalStorage('theme', 'light', { initOnMounted: true })
```

Hydration then matches, and the stored value is applied a moment later. In a Nuxt app, or any app that renders on the server, set it on every storage ref that affects what is rendered. In a plain Vite app there is no server render and you can leave it off.

In Nuxt, import `useStorage()` from `@vueuse/core` yourself, as these examples do. The `@vueuse/nuxt` module leaves it out of its auto-imports, because Nitro, Nuxt's server engine, has a `useStorage()` of its own.

## What not to store in localStorage

Settings, drafts, a dismissed banner and a last-visited tab are a good fit. Keep these out:

- **Secrets and tokens.** `localStorage` is readable by any script that runs on your page, including a third-party widget or an injected one. Keep tokens in an `HttpOnly` cookie set by the server, which scripts cannot read.
- **Large data.** Browsers cap it at a few megabytes per site, reads and writes are synchronous, and a large JSON blob is parsed on every page load.
- **Anything the user expects to keep or to see on another device.** The browser never sends `localStorage` to the server, so it stays in this one browser. Clearing the browser wipes it, and [Safari deletes it](https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/) after seven days of Safari use without the user interacting with your site. Data a user would be upset to lose belongs in a database.

**Check it stuck**3 questions from the Saving state with useStorage quiz.

[**Practice now**](https://vuejs.school/learn/vueuse/saving-state-with-usestorage?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Deep dive · 6 min read**VueUse 15's new composables: useWebMCP, useLiveAnnouncer and useTemporalNow**](https://vuejs.school/community/blog/vueuse-15s-new-composables-usewebmcp-useliveannouncer-usetemporalnow) [Release notes · 6 min read**VueUse 15: the breaking changes in plain words**](https://vuejs.school/community/blog/vueuse-15-the-breaking-changes-in-plain-words)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/keep-state-across-reloads-with-usestorage#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-03-04","description":"useStorage() from VueUse gives you a ref that saves itself to localStorage. Build a settings panel that survives a reload, and avoid the one default-value trap.","name":"Keep state across reloads with useStorage()","url":"https://vuejs.school/community/blog/keep-state-across-reloads-with-usestorage","breadcrumb":{"@id":"https://vuejs.school/community/blog/keep-state-across-reloads-with-usestorage#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/keep-state-across-reloads-with-usestorage"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/keep-state-across-reloads-with-usestorage#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-03-04","description":"useStorage() from VueUse gives you a ref that saves itself to localStorage. Build a settings panel that survives a reload, and avoid the one default-value trap.","headline":"Keep state across reloads with useStorage()","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~S2VlcCBzdGF0ZSBhY3Jvc3MgcmVsb2FkcyB3aXRoIHVzZVN0b3JhZ2UoKSDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_Keep%2Bstate%2Bacross%2Breloads%2Bwith%2BuseStorage()%2Cdescription_~dXNlU3RvcmFnZSgpIGZyb20gVnVlVXNlIGdpdmVzIHlvdSBhIHJlZiB0aGF0IHNhdmVzIGl0c2VsZiB0byBsb2NhbFN0b3JhZ2UuIEJ1aWxkIGEgc2V0dGluZ3MgcGFuZWwgdGhhdCBzdXJ2aXZlcyBhIHJlbG9hZCwgYW5kIGF2b2lkIHRoZSBvbmUgZGVmYXVsdC12YWx1ZSB0cmFwLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_sand%2Cp_Ii9jb21tdW5pdHkvYmxvZy9rZWVwLXN0YXRlLWFjcm9zcy1yZWxvYWRzLXdpdGgtdXNlc3RvcmFnZSI%2Cs_3OlvNzamu7dI4eO_.png","@type":["Article","BlogPosting"],"articleSection":["Tutorial"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/keep-state-across-reloads-with-usestorage#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/keep-state-across-reloads-with-usestorage#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/keep-state-across-reloads-with-usestorage#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Keep state across reloads with useStorage()","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~S2VlcCBzdGF0ZSBhY3Jvc3MgcmVsb2FkcyB3aXRoIHVzZVN0b3JhZ2UoKSDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_Keep%2Bstate%2Bacross%2Breloads%2Bwith%2BuseStorage()%2Cdescription_~dXNlU3RvcmFnZSgpIGZyb20gVnVlVXNlIGdpdmVzIHlvdSBhIHJlZiB0aGF0IHNhdmVzIGl0c2VsZiB0byBsb2NhbFN0b3JhZ2UuIEJ1aWxkIGEgc2V0dGluZ3MgcGFuZWwgdGhhdCBzdXJ2aXZlcyBhIHJlbG9hZCwgYW5kIGF2b2lkIHRoZSBvbmUgZGVmYXVsdC12YWx1ZSB0cmFwLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_sand%2Cp_Ii9jb21tdW5pdHkvYmxvZy9rZWVwLXN0YXRlLWFjcm9zcy1yZWxvYWRzLXdpdGgtdXNlc3RvcmFnZSI%2Cs_3OlvNzamu7dI4eO_.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~S2VlcCBzdGF0ZSBhY3Jvc3MgcmVsb2FkcyB3aXRoIHVzZVN0b3JhZ2UoKSDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_Keep%2Bstate%2Bacross%2Breloads%2Bwith%2BuseStorage()%2Cdescription_~dXNlU3RvcmFnZSgpIGZyb20gVnVlVXNlIGdpdmVzIHlvdSBhIHJlZiB0aGF0IHNhdmVzIGl0c2VsZiB0byBsb2NhbFN0b3JhZ2UuIEJ1aWxkIGEgc2V0dGluZ3MgcGFuZWwgdGhhdCBzdXJ2aXZlcyBhIHJlbG9hZCwgYW5kIGF2b2lkIHRoZSBvbmUgZGVmYXVsdC12YWx1ZSB0cmFwLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_sand%2Cp_Ii9jb21tdW5pdHkvYmxvZy9rZWVwLXN0YXRlLWFjcm9zcy1yZWxvYWRzLXdpdGgtdXNlc3RvcmFnZSI%2Cs_3OlvNzamu7dI4eO_.png"}]}
```