VueのuseStorageはどうやってlocalStorageを「リアクティブ」に扱えているのか?

この記事で解決できるお悩み
  • VueやNuxtでlocalStorageを使いたいが、リアクティブに扱えなくて困っている
  • useStorageが内部でどういう仕組みでリアクティビティを実現しているか知りたい

Vue・Nuxtでアプリを開発していると、「ページをリロードしても状態を保持したい」「タブをまたいでデータを共有したい」という場面に出くわします。

真っ先に思い浮かぶのがlocalStorageですが、ネイティブのlocalStorageはVueのリアクティビティと連動しないため、そのまま使うと意図した挙動にならないことがほとんどです。

この記事では、VueUseが提供するuseStorageどのような仕組みでlocalStorageをリアクティブに扱えるようにしているかを、内部の動作まで踏み込んで解説します。この記事を読めば、useStorageをより自信を持って使いこなせるようになります。

目次

前提:ネイティブのlocalStorageはリアクティブではない

そもそも、なぜネイティブのlocalStorageをそのまま使うと問題が起きるのでしょうか。

ブラウザのlocalStorageは、ただのキーバリューストアです。値を保存・取得することはできますが、「値が変わったら自動で検知して画面を更新する」といったリアクティブな仕組みはネイティブには存在しません。

つまり、Vueのrefreactiveと連動していないため、以下のような問題が起きます。

// ❌ これではVueの画面は更新されない
localStorage.setItem('count', '5')

Vueのテンプレートはリアクティブなデータ(refreactive)の変化に反応して再レンダリングしますが、localStorageへの書き込みはその仕組みの外側にあります。そのため、localStorageを直接更新しても画面に反映されないのです。

useStorageとは

useStorageは、VueUse(Vueのコンポーザブルユーティリティ集)が提供する関数です。

localStorageやsessionStorageをVueのリアクティブなrefとして扱えるようにしてくれます。返り値は通常のrefと同じように使え、値を変更すると自動でストレージへの保存・読み込みが行われます。

import { useStorage } from '@vueuse/core'

const count = useStorage('count', 0) // 'count'キーでlocalStorageに保存
count.value++ // これだけでlocalStorageも自動更新される

まるでVueのrefにlocalStorageの永続化機能がついたような感覚で使えます。

どういう仕組みでリアクティブを実現しているか

useStorageが実現していることを一言で表すなら、「内部のrefとlocalStorageの双方向同期」です。以下の2方向の同期によってリアクティビティを成立させています。

方向1:ref → localStorage(書き込み)

useStorageは内部にVueのリアクティブ変数(ref)を持ち、それをウォッチャー(変数を監視する仕組み)で常に監視しています。

コンポーネント側でその変数に値を代入すると、ウォッチャーが変化を検知して、ブラウザのlocalStorageへの書き込み処理を呼び出します。これによりlocalStorage.setItem()が実行され、実際にストレージへ保存されます。

なお、前回と同じ値の場合はスキップして無駄な書き込みを防ぐ最適化も施されています。

方向2:localStorage → ref(読み取り・反映)

こちらの方向はさらに2つのパターンに分かれます。

パターンA:別タブ・別ウィンドウからの変更

ブラウザにはwindowstorageイベントというネイティブ機能があります。同じオリジンの別タブがlocalStorageを変更したとき、他のタブのwindowにこのイベントが自動で発火する仕様です。

useStorageはこのイベントをaddEventListener(ブラウザ標準のイベント登録API)で購読しており、変更が通知されるとlocalStorageから値を読み直してリアクティブ変数を更新します。

また、コンポーネントが破棄されたときは自動でイベントの購読を解除する設計になっているため、メモリリークの心配もありません。

パターンB:同じタブ内の別コンポーネントからの変更

ここに一つ落とし穴があります。ブラウザのstorageイベントは、自分自身のタブでは発火しないという仕様上の制約があります。

VueUseはこの問題を、localStorageに書き込む処理の中でwindow.dispatchEvent()(イベントを手動で発火させるブラウザAPI)を使って自分でイベントを発火させることで解決しています。これにより、「コンポーネントAで値を書き込む → コンポーネントBのリアクティブ変数が自動更新」というクロスコンポーネント同期が実現されています。

型の自動判定とシリアライズ

localStorage文字列しか保存できませんが、useStoragebooleannumberobjectDateMapSetなどをそのまま扱えます。

これを支えているのがシリアライズ(型変換)の仕組みです。書き込み時はJavaScriptの値を文字列に変換(オブジェクトならJSON.stringifyなど)し、読み込み時は文字列を元の型に復元(JSON.parseなど)します。カスタムの変換処理を渡すことも可能です。

SSR(Nuxt)対応

サーバーサイドではwindowが存在しないため、localStorageに直接アクセスするとエラーになります。useStorageはサーバー側とブラウザ側で処理を切り替えられる抽象レイヤーを持っており、initOnMounted: trueオプションを使えばハイドレーション時の表示ズレを防ぐことができます。

実装例

対応バージョン

パッケージ必要バージョン
Vue3.x(Composition API必須)
Nuxt3.x(Nuxt 2はVueUse v9以前を使用)

インストール

npm install @vueuse/core

基本的な使い方(Vue 3)

<script setup>
import { useStorage } from '@vueuse/core'

// 第1引数:localStorageのキー名、第2引数:デフォルト値
const username = useStorage('username', '')
const count = useStorage('count', 0)
const settings = useStorage('settings', { darkMode: false, lang: 'ja' })
</script>

<template>
  <div>
    <input v-model="username" placeholder="名前を入力" />
    <p>{{ username }} さん、こんにちは!</p>

    <button @click="count++">カウント: {{ count }}</button>

    <label>
      <input type="checkbox" v-model="settings.darkMode" />
      ダークモード
    </label>
  </div>
</template>

まとめ

最後まで読んでいただき、ありがとうございました!

今回は、「Vue・NuxtにおけるuseStorageがローカルストレージをリアクティブに扱えている理由(仕組み)」についてご紹介しました。

「なんとなく便利だから」使っていたツールも、その仕組みを理解することで、トラブルシューティングやより高度な状態管理の設計に活かせるようになります。 もし「ブラウザを閉じてもデータを保持したい、かつVueの恩恵も受けたい」という場面があれば、ぜひuseStorageの仕組みを思い出しながら活用してみてください!

この記事が気に入ったら
フォローしてね!

シェアしていただけると大変励みになります!
  • URLをコピーしました!
  • URLをコピーしました!
目次