VueUseのuseStorageを徹底解説:window.localStorageとの比較とそのメリット

はじめに

こんにちは、フロントエンドエンジニアのraraya99です。

いきなりですが皆さんはブラウザのストレージ(localStorage)にデータを格納したいと思ったことはありませんか?

例えば、ユーザーの設定情報やフォームの入力内容を保存しておき、次回アクセス時にそのデータを自動的に読み込むといった使い方があります。 localStorageを使用すると、データをキーとバリューのペアとしてブラウザに保存し、ページがリロードされてもデータを保持できます。

このような場合、標準のWebAPIのwindow.localStorage(以下window.localStorage)を使用して実装するのが一般的です。 しかし、データの保存や取得のたびに手動でパースやシリアライズを行う必要があり、特にリアクティブな状態管理が必要な場合は手間がかかります。


私たちの業務でも、ユーザーの設定情報や一時的なデータをlocalStorageに保存することがよくあります。私たちが作成しているアプリはVue.jsで構築されており、こうした場合にはVueUseuseStorageというカスタムフックを使用しています。

しかし、なぜwindow.localStorageを使わないのか? window.localStorageと比較したuseStorageのメリットは何なのか? さらに、useStorageはどのような仕組みで動いているのか?気になる方もいるでしょう。

そこで今回はuseStorageとwindow.localStorageの比較を行い、useStorageのソースコードを読み解いてその仕組みを探ってみたいと思います!!

vueuseとは

まず、VueUseについて簡単に説明します。VueUseuseStorageは、Vue3のための便利なユーティリティ集で、アプリケーション開発を効率化するための様々なカスタムフックが含まれています。これにより、よく使われる機能を簡潔に実装できるようになります。 (vueやvite、nuxtのコアメンバーのアンソニーフーが携わっているため、信頼性が高いです。)

vueuse

useStorageの紹介

useStorageは、Vue 3のComposition APIと連携して、localStorageやsessionStorageのデータをリアクティブに扱うことができます。これにより、ストレージの状態が変更されたときに自動的にコンポーネントが再レンダリングされるため、同期の手間が省けます。

window.localStorageとuseStorageの簡単な使用方法と比較

localStorageを直接使用する場合、データの保存と取得は以下のように行います

window.localStorageを使用した場合

// データの保存
localStorage.setItem('key', JSON.stringify(data));

// データの取得
const data = JSON.parse(localStorage.getItem('key'));

この方法では、データの取得や保存ごとに手動でパースやシリアライズを行う必要があり、状態が変更された場合の再レンダリングも手動で行う必要があります。

対して、useStorageを使用すると、これらの操作が簡潔に行えるだけでなく、状態がリアクティブに保たれるため、以下のように書くことができます。

useStorageを使用した場合

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

const storageData = useStorage('key', 'default value')
</script>
<template>
  <div>
    <input v-model="storageData" />
    <p>{{ storageData }}</p>
  </div>
</template>

実際に試してみる

PlayGroundで次の保存方法を確認できる環境を作成しました。データがストレージに登録、更新されることを確認できます。

  • useStorageでlocalStorageに保存
  • useStorageを用いてsessionStorageに保存
  • window.localStorageを用いてlocalStorageに保存

実際にstorageの値を変えて色々試してみましょう。

useStorageを使うついでにsessionStorageも使用しました。 sessionStorageについてこちらを参照願います。

開発者ツールを開きアプリケーションのlocalStorageの値を確認していただけると、実際にlocalStorageに保存されるデータと表示されるデータの関係がわかりやすいと思います。(sessionStorageの値も確認できます。)

useStorageと使用した場合とwindow.localStorageを使用した場合のメリット・デメリット

useStorageのメリット

簡潔なコード: useStorageはストレージの操作を簡潔にし、コードの可読性を向上させます。

リアクティブ性: useStorageはリアクティブなデータバインディングを提供し、データが変更されると自動的にUIが更新されます。

sessionStorageのサポート: localStorageだけでなく、sessionStorageもサポートしていて簡単に実装できます。

useStorageのデメリット

依存関係: useStorageを使用するには、VueUseライブラリをインストールする必要があります。

シンプルな用途向け: 非常に複雑なストレージ操作には適していない場合があります。

window.localStorageのメリット

ライブラリ不要: 追加のライブラリを必要とせず、標準のWeb APIを使用します。

フルコントロール: 色々なカスタマイズができます。

window.localStorageのデメリット

複雑なコード: localStorageの操作を行うたびに手動でシリアライズ/デシリアライズを行う必要があり、コードが煩雑になりやすいです。

リアクティブ性の欠如: データの変更を監視してUIを更新するための追加のコードが必要です。

useStorageのソースコード解説

useStorageの便利さが伝わったと思います。この章ではuseStorageがどのようなしくみで動いているのか探っていきます。 useStorageの内部コードを確認しどのように動作しているのかを詳しく見ていきます。以下は、VueUseのリポジトリからuseStorageの実装部分を抜粋して解説します。

デフォルトのシリアライザ

デフォルトのシリアライザが定義されています。これは、JSONを使ってオブジェクトを文字列に変換し、逆に文字列をオブジェクトに変換します。

const StorageSerializers = {
  string: {
    read: (v: any) => v,
    write: (v: any) => String(v),
  },
  number: {
    read: (v: any) => Number(v),
    write: (v: any) => String(v),
  },
  boolean: {
    read: (v: any) => v === 'true',
    write: (v: any) => String(v),
  },
  object: {
    read: (v: string) => JSON.parse(v),
    write: (v: any) => JSON.stringify(v),
  },
}

useStorage関数の定義

key はストレージに保存するためのキーです。 initialValue は初期値です。 storage は使用するストレージ(デフォルトは localStorage)です。

export function useStorage<T>(
  key: string,
  initialValue: T,
  storage: Storage | undefined = defaultWindow?.localStorage,
  options: UseStorageOptions<T> = {},
) {
  const {
    deep = true,
    listenToStorageChanges = true,
    serializer = StorageSerializers.object,
  } = options

ストレージからの読み込みと初期化

read関数はストレージから値を読み込み、存在しない場合は初期値を返します。

  function read(event?: StorageEvent) {
    if (!storage) return initialValue
    const rawValue = event ? event.newValue : storage.getItem(key)
    if (rawValue == null) return initialValue
    else return serializer.read(rawValue)
  }

  const data = ref(read())

ストレージへの書き込み

値がnullの場合はストレージからキーを削除し、そうでない場合はシリアライズして保存します。

  function write() {
    if (!storage) return
    try {
      if (data.value == null) storage.removeItem(key)
      else storage.setItem(key, serializer.write(data.value))
    } catch (e) {
      console.error(e)
    }
  }

監視とイベントリスナー

watch関数は、dataが変更されたときにwriteを呼び出します。 useEventListenerはストレージの変更を監視し、変更があればデータを更新します。 useEventListener自体もvueuseのフックの1つでVueコンポーネントのライフサイクルに統合されたイベントリスナーを簡単に設定できます。

  if (isRef(initialValue) || isFunction(initialValue)) {
    watch(initialValue, (v) => {
      data.value = v
      write()
    }, { deep })
  }

  watch(data, write, { deep })

  if (listenToStorageChanges && storage) {
    useEventListener('storage', (e) => {
      if (e.key === key) data.value = read(e)
    })
  }

  tryOnScopeDispose(() => storage && write())

まとめ

これまではuseStorageをただ使用するだけでしたが、window.localStorageでの保存方法と比較することで、その便利さを再認識しました。 localStorageだけでなくsessionStorageにも簡単に保存できるのが便利。

またuseStorageのソースコードを読み解くことで、内部ではwindow.localStorageを使用していることやwindow.localStorageを使用するときのようにオブジェクトを文字列に変換し、逆に文字列をオブジェクトに変換する部分が実装されていて冗長な処理が隠匿されていることを確認できました。 useStorageを使うだけなら実装方法を意識しなくても容易に使えるようになっています。

しかし、容易に使えるからこそ雰囲気で実装してしまう可能性もあります!(戒め) vueUseにはまだまだ便利なフックがあります。

今後はその実装方法を学び、理解を深め、業務に取り入れていきたいと思います。