Vue.jsで状態管理する方法!(グローバルなストアを作成)

Vue.js Vue.js

Vue.jsのprovideとinjectを使用して、データの状態管理をするストアを作成する方法です。
Vuexを使用しなくてもアプリケーションの状態管理が可能になります。

サンプルコードの検証には下記のバージョンを使っています。
Vue.js 3.2.31 Composition API
Vue CLI 4.5.13

ストアの実装

グローバルなデータストアとして、サンプルでユーザー情報を保持するストアを作成しました。

最初にユーザー情報のストア実装サンプルを書いていて、その後に作ったストアの情報を取得するためのグローバルなストアの実装を載せています。
最後に作成したストアをVue.jsのアプリケーションマウント時に、provideを使用して設定します。

ユーザー情報を保持するストアの実装

ユーザーのストアを下記のように実装しました。
storeというディレクトリを作成して、その配下にuser.jsという名前で用意しました。

import { reactive } from "vue"

export default function userStore() {
  const state = reactive({
    users: [],
  })

  return {
    get users() {
      return state.users
    },

    set users(values) {
      state.users = values
    },
  }
}

userStoreという名前で、関数を作成してexportしています。

関数の中で、state変数をVue.jsのreactiveで作っています。
保持する内容として、usersプロパティを持っています。
ここにユーザーの情報のオブジェクトを配列で持つ予定です。

あとは内容を取得するためのgetterと、内容を更新するためのsetterを作成しています。

グローバルなストアの実装

グローバルなストアの実装が下記になります。
storeディレクトリ配下にglobal.jsという名前で用意しました。

import userStore from "./user"
import { inject } from "vue"

export function globalStore() {
  return {
    user: userStore(),
  }
}

export const GlobalStoreKey = Symbol('GlobalStore')

export function useGlobalStore() {
  const store = inject(GlobalStoreKey)
  if (!store) {
    throw new Error(`${GlobalStoreKey} is not provided`)
  }
  return store
}

関数を2つ作成しています。

globalStore関数は、アプリケーションマウント時にprovideする箇所で使用します。
内容としては、先ほど作成したユーザーのストアを取得して返すようにしています。

useGlobalStore関数はinjectを使用して、Symbolで作成しているキーでストアの内容を取得しています。
Symbolはユニークなキーのような役割で使用することができます。

GlobalStoreKeyという名前のSymbolを使用して、provideをマウント時にするようにします。
ここで、同じようにGlobalStoreKeyを使用してinjectで登録されたストアのデータを返すようにしています。

Vue.jsのマウント時に設定する

Vue.jsをマウントする箇所でprovideを使用して、下記のようにストアを登録しています。
Vue CLIでプロジェクトを作成した場合はmain.jsファイルになります。

import { createApp } from 'vue'
import { GlobalStoreKey, globalStore } from './store/global.js'
import App from './App.vue'
import router from './router/index'

const app = createApp(App)
app.provide(GlobalStoreKey, globalStore()).use(router).mount('#app')

最初にimport文を使用して、必要なモジュールなどを読み込みます。
最初にcreateAppを読み込んで、次に先ほど作成したGlobalStoreKeyとglobalStoreを読み込みました。

Appはアプリケーションのエントリーコンポーネントで、router-viewを書いているコンポーネントになります。
routerはVue Routerで使用しているルーティングです。

最後にcreateAppを使用して、アプリケーションをHTMLにマウントしています。
このマウントする箇所の下記で、provivdeを使用して作成したストアをGlobalStoreKeyで登録しました。

app.provide(GlobalStoreKey, globalStore())

これでuseGlobalStore関数を呼べば、一意のGlobalStoreKeyで登録したglobalStoreの内容が取得できるようになります。

ストアをテストするためのコンポーネント

ストアをテストするためのコンポーネントをいくつか用意しました。
トップページでユーザー情報(名前・年齢)を入力して、他のページに遷移したときにストアから情報を取って表示できるかを試してみました。

コンポーネントの見た目の実装ではTailwindCSSを使っています。

ユーザー情報を登録するコンポーネント

ユーザー情報を登録して表示するコンポーネントを下記のように用意しました。

<template>
  <div class="m-5">
    <div class="mb-5">
      TopPage
    </div>
    <div class="mb-2">
      名前:<input type="text" class="rounded border border-blue-300 px-3 py-1" size="10" v-model="userName" /> 
      年齢:<input type="text" class="rounded border border-blue-300 px-3 py-1" size="10" v-model="userAge" />
      <button class="rounded px-3 py-1 ml-2 border-blue-300 border-2 bg-blue-700 text-white" @click="addUserStore">
        追加
      </button>
    </div>
    <div class="border-2 border-gray-300 w-5/12 rounded p-2 mb-2">
      <div v-for="user in userStore.users" :key="user.user">
        <ul>
          <li>名前:{{ user.name }}</li>
          <li>年齢:{{ user.age }}</li>
        </ul>
      </div>
    </div>
    <ul>
      <li class="mb-1"><router-link to="/store-test"><button class="rounded border-2 border-blue-300 p-2">storeテストコンポーネントへ</button></router-link></li>
      <li class="mb-1"><router-link to="/store-test2"><button class="rounded border-2 border-blue-300 p-2">storeテスト2テストコンポーネントへ</button></router-link></li>
      <li><router-link to="/store-test3"><button class="rounded border-2 border-blue-300 p-2">storeテスト3テストコンポーネントへ</button></router-link></li>
    </ul>
  </div>
</template>
<script>
import { ref } from "vue"
import { useGlobalStore } from "@/store/global"

export  default({
  setup() {
    const { user: userStore } = useGlobalStore();

    const userName = ref('')
    const userAge = ref('')

    const addUserStore = () => {
      userStore.users.push({ name: userName.value, age: userAge.value })
      userName.value = ''
      userAge.value = ''
    }

    return {
      userStore,
      addUserStore,
      userName,
      userAge
    }
  }
})
</script>

タグの箇所の解説

タグの箇所についてです。
まず、名前と年齢を入力して、登録するエリアを作成しています。
登録ボタンを押すと、入力した内容が反映される想定です。

名前:<input type="text" class="rounded border border-blue-300 px-3 py-1" size="10" v-model="userName" /> 
年齢:<input type="text" class="rounded border border-blue-300 px-3 py-1" size="10" v-model="userAge" />
<button class="rounded px-3 py-1 ml-2 border-blue-300 border-2 bg-blue-700 text-white" @click="addUserStore">
  追加
</button>

そして、下記で登録内容を表示しています。

<div v-for="user in userStore.users" :key="user.user">
  <ul>
    <li>名前:{{ user.name }}</li>
    <li>年齢:{{ user.age }}</li>
  </ul>
</div>

あとは、それぞれのコンポーネントへのリンクを作成しています。

スクリプトの箇所の解説

まず、下記で作成したuseGlobalStore関数を使用して、ユーザーストアの内容を取得しています。
userを取得して、userStoreという名前で使用するようにしています。

const { user: userStore } = useGlobalStore();

その後に、入力テキストエリアにv-modelするために下記の2つの変数をrefで作成して、画面で使うようにしています。

const userName = ref('')
const userAge = ref('')

そして、addUserStore関数を作っています。

const addUserStore = () => {
  userStore.users.push({ name: userName.value, age: userAge.value })
  userName.value = ''
  userAge.value = ''
}

ユーザー情報を入力して追加ボタンを押されたときに動く関数です。
関数の中では、userStoreのusersに入力した内容を反映させています。
追加したあとは、テキストボックスを空にするために、変数に空文字を設定しています。

ユーザー情報を表示するコンポーネント

次に下記のコンポーネントを作成しました。
追加された情報が、他の画面でも参照できるか確認するためのコンポーネントです。

<template>
  <div class="m-5">
    <div class="mb-5">storeのテストコンポーネント1</div>
    <div class="border-2 border-gray-300 w-5/12 rounded p-2 mb-2">
      <div v-for="user in userStore.users" :key="user.user">
        <ul>
          <li>名前:{{ user.name }}</li>
          <li>年齢:{{ user.age }}</li>
        </ul>
      </div>
    </div>
    <div class="mb-1">
      <router-link to="/">
        <button class="rounded border-2 border-blue-300 p-2">
          トップページへ
        </button>
      </router-link>
    </div>
    <div class="mb-1">
      <router-link to="/store-test2">
        <button class="rounded border-2 border-blue-300 p-2">
          storeテストコンポーネント2へ
        </button>
      </router-link>
    </div>
    <div>
      <router-link to="/store-test3">
        <button class="rounded border-2 border-blue-300 p-2">
          storeテストコンポーネント3へ
        </button>
      </router-link>
    </div>
  </div>
</template>
<script>
import { useGlobalStore } from "@/store/global";
export default {
  setup() {
    const { user: userStore } = useGlobalStore();

    return {
      userStore,
    };
  },
};
</script>

解説

タグの方では、ストアのデータをv-forを使用して表示させています。
その後に、他のコンポーネントへのリンクを作成しています。

スクリプトの方では、作成したuseGlobalStoreを読み込んできています。
読み込んだ後に、ユーザーストアの情報を画面に返しているだけです。

const { user: userStore } = useGlobalStore();

return {
  userStore,
}

トップ画面で入力したデータが、userStore.usersに格納されるので、それをループして表示できる想定です。

他にも動作確認のため、同様の内容のコンポーネントを2つ作りましたが、内容が同じなので割愛いたします。
(/store-test2と/store-test3への遷移先です)

動作確認

ブラウザを起動して、動作を確認します。

トップページでユーザーの情報を入力して登録すると、下記のようになります。
Vue.jsでstoreの確認(情報登録)
作成したユーザーストアを取得して、そこにデータを入れています。

その後にリンクで画面遷移すると、下記のように情報が保持されたまま遷移できることが確認できました。
Vue.jsでstoreの確認(画面遷移)

参考

今回はVue.jsのprovideとinjectを使用して、データストアを作成してみました。
TypeScriptでの実装がQiitaのこちらに書いていたので、参考にさせてもらいました。

コメント

タイトルとURLをコピーしました