🙄

Vue3と状態管理のPinia

2024/04/08に公開

Vue3とともに「Pinia」という新しいツールが登場しました。
Vuexに代わりPiniaが状態管理の推奨ライブラリとなりました。
当記事ではPiniaの使い方について解説しています。
※私はVue3から学習始めた身なので、Vuexのことは分かりません

状態管理ツールは何のためにあるのか?

まず始めに、Piniaは何のために使うのでしょうか?
Piniaは、アプリケーションの状態管理を助けるために使用されます。では、「状態」とは何でしょうか?
「状態」とはアプリ内でユーザーの操作や外部からのデータ取得などによって変化するデータのことを言います。

例えば、ユーザーがログインしていない場合は「ログイン」ボタンを表示し、既にログインしている場合はそのボタンを隠すとします。表示されるボタンは、ユーザーがログインしているかどうかの”状態”に依存します。

しかしコンポーネントが多数存在するVueアプリ全体で、どのようにしてこのような状態を管理するのでしょうか?
各コンポーネントでpropsを通して下位のコンポーネントに渡すことも可能ですが、その状態をコンポーネント間で何重にも渡していくのはバケツリレー状態になってしまい、処理を追いづらくもなります。
また。あるコンポーネントがその状態について別のコンポーネントツリーの全く異なる部分にあるコンポーネントに伝える必要がある場合はどうしたらよいのかという問題も発生します。

この問題を解決するのが状態管理ツールになります。
アプリケーションのグローバル状態を管理し、そのグローバル状態がアプリ内のどのコンポーネントからもアクセス可能にするということを行なっています。
この状態管理ツールがPiniaになります。

Piniaの概要

Piniaを使用することで、アプリケーションの状態を簡単に保存、更新、取得することができます。
VueコンポーネントとPiniaストアの関係を簡単に比較すると以下のようになっています。

Vueコンポーネントでは、ローカルデータをdataプロパティやrefで持ちますが、Piniaストアではアプリケーション全体で共有される状態をstateプロパティで管理します。
コンポーネントがデータを更新するmethodsを持つように、Piniaストアではactionsを通じて状態を更新します。
そして、コンポーネントがcomputedプロパティでデータを加工するのと同様に、Piniaストアではgettersを使って状態の加工バージョンを提供します。

Vueコンポーネント Piniaストア 説明
dataプロパティ stateプロパティ アプリ内で管理されるデータ。Vueではコンポーネントごと、Piniaではアプリ全体で共有。
methods actions データを更新するための関数。
computedプロパティ getters 加工されたデータを返す。データ自体は変更しない。

実際にコードに起こすと以下のイメージです

import { defineStore } from 'pinia'
export const useAuthStore = defineStore('AuthStore', {
  state: () => ({
    user: null
  }),
  actions: {
    loginUser(user) {
      this.user = user
    },
    logoutUser() {
      this.user = null
    }
  },
  getters: {
    isAuthenticated: (state) => state.user !== null
  }
})

Piniaを触ってみる

Vueのプロジェクトを作成する際にも、Piniaを使うかを聞かれます。
もしくはnpmからでもinstallしましょう npm install pinia

ここでは簡単な例としてTodoアプリを作成します。

<template>
  <div>
    <input v-model="todoText" @keyup.enter="addAndClear" placeholder="What needs to be done?">
    <ul>
      <li v-for="todo in store.todos" :key="todo.id">
        <label>
          <input type="checkbox" :checked="todo.done" @change="() => store.toggleTodo(todo.id)">
          {{ todo.text }}
        </label>
      </li>
    </ul>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import { useTodoStore } from './stores/todoStore';

const todoText = ref('');
const store = useTodoStore();

const addAndClear = () => {
  if (todoText.value.trim()) {
    store.addTodo(todoText.value);
    todoText.value = '';
  }
}
</script>

Pinitを使ったstoreは以下

import { defineStore } from 'pinia';

export const useTodoStore = defineStore('todoStore', {
  state: () => ({
    todos: []
  }),
  actions: {
    addTodo(text) {
      this.todos.push({ id: Date.now(), text, done: false });
    },
    toggleTodo(id) {
      const todo = this.todos.find(todo => todo.id === id);
      if (todo) {
        todo.done = !todo.done;
      }
    }
  },
  getters: {
    completedTodos: state => state.todos.filter(todo => todo.done),
    pendingTodos: state => state.todos.filter(todo => !todo.done)
  }
});

Discussion