はじめに
この記事は、every Tech Blog Advent Calendar 2024(夏) の1日目の記事です。
DELISH KITCHEN開発部の羽馬(@NaokiHaba)です。
この記事では、DELISH KITCHEN チラシ で使用している Vuex の Pinia への移行について紹介します。
本記事では、これらの知識があることを前提に説明を進めます。
- Vue.jsの基本的な知識
- Nuxt.jsの基本的な知識
- Vuexの基本的な知識
Piniaとは
Pinia(ピーニャ)は、Vue.js用の新しい状態管理ライブラリです。Vuexの次のイテレーションとして開発が始まり、Vuex 5に組み込むことを想定していたアイデアを多く取り入れています。
Piniaは、Vuexと比較して以下のような特徴や利点があります。
- シンプルなAPIを提供し、学習コストが低い
- TypeScriptとの連携が強化され、型の恩恵を受けやすい
- モジュール方式を採用せず、ストアを個別に定義できるため、コードの可読性や保守性が向上する
- Vue Devtoolsとの統合が進んでおり、開発体験が良い
Piniaは、Vue.js v2とv3の両方に対応しており、Nuxt.jsにも対応しています。Nuxt v3からは、VuexからPiniaが公式に推奨されるようになりました。
なぜPiniaに移行するのか
DELISH KITCHEN チラシ では、以下の理由からPiniaへの移行を決定しました。
- Nuxt3への移行を見据えて、早めにPiniaを導入しておきたかった
- Vuex は現在メンテナンスモードであり、今後のアップデートが見込めないため
- Nuxt3以降もPiniaの公式サポートが続くと予想されるため
Piniaへの移行によって、Nuxt3への移行をスムーズに進めることができると考えました。
移行の手順
1. Piniaの導入
まずは、Pinia を導入します。
$ yarn add pinia @pinia/nuxt # or with npm $ npm install pinia @pinia/nuxt
次に、nuxt.config.js に Pinia の設定を追加します。 移行時点では、Vuex と Pinia を併用することとなるため、disableVuex を false に設定します (disableVuex はデフォルトで true になっているため、Vuex が無効化されます)
// nuxt.config.js export default defineNuxtConfig({ buildModules: [ // set `disableVuex` to false if you need to use Vuex alongside Pinia [ '@pinia/nuxt', { disableVuex: false } ], ], })
以上で、Pinia の導入は完了です。
2. VuexストアのPiniaストアへの移行
次に、既存のVuexストアをPiniaストアに移行します。 Piniaでは、ストアをdefineStore関数を使って定義します。defineStore関数には、ストアの名前を表すidと、ストアの定義を表すoptionsの2つの引数を渡します。
以下は、Vuexストアの例です。
// store/todo.js export default { state: { todos: [], }, mutations: { setTodos(state, todos) { state.todos = todos }, }, actions: { async fetchTodo({ commit }, id) { try { const response = await this.$axios.get(`https://jsonplaceholder.typicode.com/todos/${id}`) commit('setTodos', [ response.data ]) } catch (error) { console.error(error) } }, }, getters: { allTodos: state => state.todos, }, }
このVuexストアを、Piniaストアに移行すると以下のようになります。
// stores/todo.js import { defineStore } from 'pinia' export const useTodoStore = defineStore('todos', { state: () => ({ todos: [], }), actions: { async fetchTodo(id) { try { const response = await this.$nuxtAxios.get(`https://jsonplaceholder.typicode.com/todos/${id}`) this.todos = [ response.data ] } catch (error) { console.error(error) } }, }, getters: { allTodos: (state) => state.todos, }, })
Piniaストアでは、mutationsが削除され、actionsとgettersのみが残っています。これは、Piniaではmutationsの概念がなくなり、actionsで直接ステートを更新するためです。
また、actions内でのthisの扱いが変わっています。Piniaでは、thisがストアのインスタンスを指すため、this.todosのように直接ステートを更新できます。
ここで、this.$axios
が this.$nuxtAxios
に変更されていることに注目してください。
Piniaでは、ストアの中で this
がストアのインスタンスを指します。したがって、Vuexストアで使っていた this.$axios
をそのまま使うことはできません。
代わりに、Nuxtのコンテキストからプラグインを介して $axios
を取得し、this.$nuxtAxios
として使用しています。
このプラグインは、以下のように定義します。
// plugins/pinia-inject-axios.js export default defineNuxtPlugin((nuxtApp) => { nuxtApp.$pinia.use(() => ({ $nuxtAxios: markRaw(nuxtApp.$axios), })); });
そして、nuxt.config.js でこのプラグインを登録します。
Nuxt3では、$fetch
を使うことが推奨されており、@nuxtjs/axios
は利用できないため、このプラグインは不要になります。
// nuxt.config.js export default defineNuxtConfig({ plugins: [ '~/plugins/pinia-inject-axios.js', ], })
nuxtServerInit の扱い
Vuexでは、nuxtServerInit
はサーバーサイドレンダリング(SSR)時に、サーバー側での初期化処理を行うための特別なアクションでした。Nuxt.jsでは、SSR時にstore
ディレクトリ内の各ストアのnuxtServerInit
アクションが自動で呼び出される仕組みがあります。
一方、PiniaではnuxtServerInit
が自動で呼び出される仕組みがありません。代わりに、plugins
やmiddleware
を利用して、nuxtServerInit
の処理を移行する必要があります。
例えば、plugins/nuxt-server-init.js
というファイルを作成し、以下のようなコードを記述します。
export default defineNuxtPlugin(nuxtApp => { if (process.server) { // サーバー側での初期化処理をここに記述 } })
3. コンポーネント内でのストアの利用方法の変更
最後に、コンポーネント内でのストアの利用方法を変更します。 VuexではmapState、mapGetters、mapActionsなどのヘルパー関数を使ってストアにアクセスしていました。 Piniaでも同様のヘルパー関数が用意されていますが、mapGettersの代わりにmapStateを使うことが推奨されています。
<template> <div> <div v-for="todo in todos" :key="todo.id"> {{ todo.title }} </div> <button @click="fetchTodo(1)">Fetch Todo</button> </div> </template> <script> import { mapState, mapActions } from 'pinia' import { useTodosStore } from '~/stores/todosStore' export default { fetch({ app, error, $pinia }) { const todosStore = useTodosStore($pinia) todosStore.fetchTodo(1) }, computed: { ...mapState(useTodosStore, [ 'todos' ]), }, methods: { ...mapActions(useTodosStore, [ 'fetchTodo' ]), }, } </script>
Composition APIを使う場合は、useStore関数を使ってストアのインスタンスを取得し、直接ストアの状態やアクションにアクセスできます。
<script setup> import { useTodosStore } from '~/stores/todosStore' const todosStore = useTodosStore() await todosStore.fetchTodo(1) </script> <template> <div> <div v-for="todo in todosStore.todos" :key="todo.id"> {{ todo.title }} </div> <button @click="todosStore.fetchTodo(1)">Fetch Todo</button> </div> </template>
まとめ
この記事では、DELISH KITCHEN チラシ におけるVuexからPiniaへの移行について紹介しました。 Piniaは、Vuexと比べてシンプルなAPIを提供し、TypeScriptとの連携が強化されているため、Nuxt3での開発をスムーズに進めることができます。 Nuxt3での開発を行う際には、ぜひPiniaの導入を検討してみてください。