every Tech Blog

株式会社エブリーのTech Blogです。

Vuex から Pinia への移行を行いました

はじめに

この記事は、every Tech Blog Advent Calendar 2024(夏) の1日目の記事です。

DELISH KITCHEN開発部の羽馬(@NaokiHaba)です。

この記事では、DELISH KITCHEN チラシ で使用している Vuex の Pinia への移行について紹介します。

chirashi.delishkitchen.tv

本記事では、これらの知識があることを前提に説明を進めます。

  • Vue.jsの基本的な知識
  • Nuxt.jsの基本的な知識
  • Vuexの基本的な知識

Piniaとは

Pinia(ピーニャ)は、Vue.js用の新しい状態管理ライブラリです。Vuexの次のイテレーションとして開発が始まり、Vuex 5に組み込むことを想定していたアイデアを多く取り入れています。

pinia.vuejs.org

Piniaは、Vuexと比較して以下のような特徴や利点があります。

  • シンプルなAPIを提供し、学習コストが低い
  • TypeScriptとの連携が強化され、型の恩恵を受けやすい
  • モジュール方式を採用せず、ストアを個別に定義できるため、コードの可読性や保守性が向上する
  • Vue Devtoolsとの統合が進んでおり、開発体験が良い

Piniaは、Vue.js v2とv3の両方に対応しており、Nuxt.jsにも対応しています。Nuxt v3からは、VuexからPiniaが公式に推奨されるようになりました。

なぜPiniaに移行するのか

DELISH KITCHEN チラシ では、以下の理由からPiniaへの移行を決定しました。

  1. Nuxt3への移行を見据えて、早めにPiniaを導入しておきたかった
  2. Vuex は現在メンテナンスモードであり、今後のアップデートが見込めないため
  3. Nuxt3以降もPiniaの公式サポートが続くと予想されるため

Piniaへの移行によって、Nuxt3への移行をスムーズに進めることができると考えました。

移行の手順

1. Piniaの導入

まずは、Pinia を導入します。

pinia.vuejs.org

$ 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つの引数を渡します。

pinia.vuejs.org

以下は、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.$axiosthis.$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が自動で呼び出される仕組みがありません。代わりに、pluginsmiddleware を利用して、nuxtServerInitの処理を移行する必要があります。

例えば、plugins/nuxt-server-init.jsというファイルを作成し、以下のようなコードを記述します。

export default defineNuxtPlugin(nuxtApp => {
  if (process.server) {
    // サーバー側での初期化処理をここに記述
  }
})

3. コンポーネント内でのストアの利用方法の変更

最後に、コンポーネント内でのストアの利用方法を変更します。 VuexではmapState、mapGetters、mapActionsなどのヘルパー関数を使ってストアにアクセスしていました。 Piniaでも同様のヘルパー関数が用意されていますが、mapGettersの代わりにmapStateを使うことが推奨されています。

pinia.vuejs.org

<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の導入を検討してみてください。