Vueの状態管理にはVuexやPiniaを使いましょう、と言われるが外部のライブラリ使わなくてもVue3本体の機能だけで十分では?むしろそっちの方がメリット多くない?という話。
状態管理ライブラリ使わずにどうやってストア(状態とアクション)を定義するの?
Vue3から導入されたComposition APIを使えばできます。例えば
import { reactive, computed, readonly } from 'vue';
export function useTodo() {
const items = reactive([]);
const firstItem = computed(() => items[0] ?? null);
function add(todo) {
items.push(todo);
}
return { items: readonly(items), firstItem, add };
}
Composition APIというVue本体の機能だけで簡単に状態とアクションが定義できました。reactive
, ref
, computed
, watch
, readonly
などの関数を駆使することで状態管理ライブラリでできることはおよそ全てできるどころか、Composition APIの方がより柔軟かつシンプルに書けている思います。
どうやってストアをコンポーネントを跨いでグローバルに共有できるようにするの?
Vue本体の機能であるprovide
とinject
を使えばできます。provide
関数とinject
関数はVueの依存関係注入の仕組みで、親のコンポーネントでprovide('key', value)
で定義した値をその子コンポーネントからconst value = inject('key')
とすることで取り出すことができます。
なのでアプリ全体で共有するストアはトップのApp
コンポーネント内で生成してprovide
すれば、そのストアを使いたい各コンポーネントはinject
で使うことができます。
<template>
<router-view />
</template>
<script>
import { provide } from 'vue';
import { useTodo } from '~/composables/todo.js';
export default {
setup() {
const todoStore = useTodo();
provide('todo', todoStore);
},
};
</script>
<template>
<ul>
<li v-for="item of todos">{{ item }}</li>
</ul>
</template>
<script>
import { inject } from 'vue';
export default {
setup() {
const todo = inject('todo');
return { todos: todo.items };
},
};
</script>
この仕組みを使えばグローバルに共有するだけでなく、特定のコンポーネントツリーの中だけで共有するストアも作れます。規模の大きなアプリで、ある機能群の中だけでストアを共有するとかできるかもしれません。VuexやPiniaではこのようなことはできません。
あるストア内から別のストアを参照したいときどうするの?
ストアを生成するときに参照したいストアを引数で渡します。例えば以下のuseTodoSearch
関数のようにします。
<template>
<input v-model="searchQuery" />
<ul>
<li v-for="item of todos">{{ item }}</li>
</ul>
</template>
<script>
import { inject } from 'vue';
import { useTodoSearch } from '~/composables/todo-search.js';
export default {
setup() {
const todo = inject('todo');
const todoSearch = useTodoSearch(todo);
return { searchQuery: todoSearch.query, todos: todoSearch.matched };
},
};
</script>
useTodoSearch
の実装は以下のようになります。
import { ref, computed } from 'vue';
export function useTodoSearch(todoStore) {
const query = ref('');
function matches(q, todo) {
// todoが検索クエリqにマッチするかどうかを返す関数
}
const matched = computed(() => todoStore.items.filter(
item => matches(query.value, item)
));
return { query, matched };
}
これによってストア間の依存関係が明確になるというメリットがあります。VuexやPiniaではストアはグローバルにどこからでも参照できるのでストア間の関係性は不明です。VuexやPiniaのストアは本質的にはあの忌避すべきグローバル変数であり、あらゆる場所から状態が変更されうるし、あらゆる場所でその状態に依存した処理が書けてしまいます。
状態管理ライブラリの利点
VuexやPiniaなど個別の状態管理ライブラリを使う利点としては、高機能なデバッグツールが挙げられます。ブラウザの拡張機能を入れることで各ストアの現在の状態を一発で確認したり、過去の状態に遡ったりもできたりします。ここで解説したVue3のみを使った方法ではそのようなことはできません。
また、私は詳しくわかってないですが、SSR(Server-Side Rendering)等への対応が最初からなされていて自分で色々やる必要がないというメリットもあるのかもしれません。
あとがき
Composition APIは非常にスマートにストアを定義できていると思います。VuexやPiniaはVue2のOptions APIチックで論理的な機能の分離ができず読みづらいです。また外側から見たときに、状態を根源的な一次状態state
とそこから派生した二次状態getter
に分離したり、アクションも同じようにmutation
とaction
に分けたりしていますが、これらの機能もストアの使い方を無意味に複雑化しているだけ感があり、好きではありません(特にVuex)。(PiniaはComposition APIでも定義できる模様、公式ドキュメントでほとんど解説ないので謎だけど)
Vue.js devtoolsがもっと進化してVue本体だけでもデバッグがしやすくなるとベストなんですけどね。それが難しいから状態管理ライブラリは変な定義方法を要求するのかねえ。