JavaScriptのフレームワークであるVueは、ユーザーインターフェースやシングルページアプリケーション(SPA)を構築する上で人気の選択肢です。大規模なアプリケーションを最適化し効率的に機能させるには、状態管理(アプリケーションのリアクティブなデータを複数のコンポーネントにわたって管理し、一元化するプロセス)をしっかりと理解する必要があります。

Vueでは、状態管理は長い間Vuexに依存してきました。Vuexは、アプリケーションのすべてのコンポーネントを一元的に保存するライブラリです。しかし、最近のVueエコシステムの進歩により、Vuexの後継であるPiniaが登場しました。

Piniaは、より軽量でモジュールに基づいた直感的な管理を可能にします。Piniaは、VueのリアクティビティシステムやComposition APIとシームレスに統合されており、スケーラブルで保守性の高い方法で共有状態を管理したりアクセスしたりすることができます。

PiniaとVuexの基本的な違い

Vueアプリケーションの状態管理に便利なライブラリとして、Vuexがアプリケーションのすべてのコンポーネントを一元管理する役割を果たしてきました。しかし、Vueの進化に伴い、よりモダンなソリューションとしてPiniaが登場しています。それでは、Vuexとの違いをおさらいしてみましょう。

  • APIの違い:PiniaのComposition APIはVuexよりも基本的で直感的なAPIを提供し、アプリケーションの状態をより簡単に管理できます。さらに、その構造はVueのOptions APIによく似ており、多くのVue開発者にとって親しみのあるものとなっています。
  • 型付けのサポート:過去、多くのVue開発者がVuexの限られた型サポートに苦労してきました。対照的に、Piniaは完全に型付けされた状態管理ライブラリであり、このような懸念を解消しています。その型安全性は、潜在的な実行時エラーを防ぎ、コードの可読性に貢献し、スムーズなスケーリングを促進します。
  • リアクティブシステム:両ライブラリともVueのリアクティブシステムを活用していますが、PiniaはVue 3のComposition APIにより近いものとなっています。リアクティブAPIは優れものですが、大規模なアプリケーションで複雑な状態を管理するのは困難です。幸いなことにPiniaのわかりやすく柔軟なアーキテクチャにより、Vue 3アプリケーション状態管理における手間が軽減しています。Piniaのstoreパターンを使用すると、アプリケーションの状態の特定の部分を管理するstoreを定義して、その構成を簡素化し、コンポーネント間で共有することができます。
  • 軽量性:Piniaはわずか1 KBで開発環境にシームレスに統合でき、その軽量性によりアプリケーションのパフォーマンスと読み込み時間の向上が期待できます。

PiniaでVueプロジェクトをセットアップする方法

PiniaをVueプロジェクトに統合するには、Vue CLIまたはViteでプロジェクトを初期化します。プロジェクトの初期化後、依存関係としてnpmまたはyarn経由でPiniaをインストールできます。

  1. Vue CLIまたはViteを使用して新しいVueプロジェクトを作成します。次に、プロンプトに従ってプロジェクトをセットアップします。
    // Vue CLIを使用する
    vue create my-vue-ap
    // Viteを使用する
    npm create vite@latest my-vue-app -- --template vue
  2. ディレクトリを作成したプロジェクトフォルダに変更します。
    cd my-vue-app
  3. プロジェクトの依存関係としてPiniaをインストールします。
    // npmを使用する
    npm install pinia
    // yarnを使用する
    yarn add pinia
  4. メインのエントリファイル(通常はmain.jsmain.ts)でPiniaをインポートして、VueでのPinia使用を指定します。
    import { createApp } from 'vue';
    import { createPinia } from 'pinia';
    import App from './App.vue';
    
    const app = createApp(App);
    
    app.use(createPinia());
    app.mount('#app');

    PiniaのインストールとVueプロジェクトのセットアップが完了したら、状態管理にstoreを定義して使用する準備が整いました。

Piniaでstoreを作成する方法

storeは、Piniaを使用したVueアプリケーションの状態管理に欠かせない要素です。storeは、アプリケーション全体のデータを統合的かつ協調的に管理するのに力を発揮します。storeを使って、アプリケーションのさまざまなコンポーネント間で共有するデータを定義、保存、管理することになります。

この一元化は非常に重要で、アプリの状態変化を構造化して整理し、データフローをより予測しやすくデバッグしやすくします。

さらに、Piniaのstoreは状態を保持するだけではありません。Pinia内の機能により、アクションによって状態を更新したり、ゲッターによって派生状態を計算したりできます。これらの組み込みの機能を活用することで、より効率的で保守性の高いコードの保持が可能になります。

次の例では、基本的なPiniaのstoreをプロジェクトのsrc/store.jsファイルに作成しています。

import { defineStore } from 'pinia';
export const useStore = defineStore('main', {
    state: () => ({
        count: 0,
    }),
    actions: {
        increment() {
            this.count++;
        },
    },
    getters: {
        doubleCount: (state) => state.count * 2,
    },
});

コンポーネントでstoreの状態にアクセスする方法

Vuexと比較して、Piniaの状態へのアクセスと管理のしかたはより直感的です。このAPIは、コンポーネントにリアクティブかつコンポーザブルなロジックを組み込むことができます。

次のコードについて考えてみましょう。

<template>
	<div>{{ store.count }}</div>
</template>

<script>>
import { useStore } from './store';

export default {
	setup() {
	const store = useStore();
		return { store };
	},
}
</script>

上記のコードでは、<template>タグにコンポーネントの定義されたHTMLマークアップが含まれています。Pinia storeからcountプロパティの値を表示するには、Vueのデータバインディング構文{{ count }}を使用します。

useStore関数によりPinia storeへのアクセスが可能になります。これを使うために、import { useStore } from './store';を使用してstore.jsからインポートします。

Vue 3のComposition APIの機能であるsetup関数は、コンポーネントのリアクティブな状態とロジックを定義します。この関数内でuseStore()を呼び出し、Pinia storeにアクセスします。

次に、const count = store.countがstoreのcountプロパティにアクセスし、コンポーネントで利用できるようにします。

最後に、setupcountを返し、テンプレートがそれをレンダリングできるようになります。Vueのリアクティブシステムでは、storeの値が変更されるたびに、コンポーネントのテンプレートがcountの値を変更します。

出力のスクリーンショットが以下の通りです。

Pinia Storeデモアプリランディングページのスクリーンショット
ブラウザで読み込んだPinia Store Demoテンプレートのスクリーンショット

この例から、Piniaの以下のような利点がわかります。

  • シンプル:Piniaでは、マッピング関数なしでstoreの状態に直接アクセスできます。対照的に、Vuexでは同じアクセスを実現するためにmapState (または同様のヘルパ)が必要になります。
  • storeへの直接アクセス:Piniaでは(store.countのような)状態プロパティに直接アクセスできるので、コードが読みやすく理解しやすくなります。一方、Vuexでは基本的なプロパティにアクセスするにもゲッターが必要な場合が多く、複雑さが増して可読性が低下します。
  • Composition APIとの互換性:セットアップメソッドが示すように、PiniaはComposition APIと統合されているため、最新のVue開発と高い親和性を誇ります。

Piniaで状態を変更する方法

Piniaでは、アクションを使用してstoreの状態を変更します。アクションはVuexのミューテーションよりも柔軟です。状態のcountプロパティをインクリメントする、次の関数呼び出しを考えてみましょう。

store.increment(); // カウントをインクリメントする

一方、Vuexでは、少なくとも1つのアクションに加えてミューテーションを定義する必要があります。

mutations: {
	increment(state) {
	state.count++;
	},
},
actions: {
	increment({ commit }) {
	commit('increment');
	},
}

Piniaのアクションと同等のVuexのコードからは、ライブラリのコードの複雑さの決定的な違いがわかります。これらの違いをさらに詳しくご紹介します。

  • 直接的な状態変更と間接的な状態変更incrementアクションが示すように、Piniaのアクションはstoreの状態を直接変更することができます。Vuexでは、ミューテーションをアクションでコミットすることで同じ操作を行います。このプロセスの分離により、状態の変更を追跡できるようになりますが、同等のPiniaにおけるアクションよりも複雑で堅苦しさがあります。
  • 非同期処理と同期処理:Vuexのミューテーションは常に同期処理であり、非同期処理を含めることができませんが、Piniaのアクションは同期および非同期どちらのコードも扱えます。その結果、アクション内でAPIコールやその他の非同期処理を直接実行できるためコードがスリムで簡潔になります。
  • 簡素な構文:Vuexでは、しばしばミューテーションを定義し、それをコミットするためにアクションを呼び出す必要があります。Piniaではこの必要性が取り除かれています。アクション内で状態を変更できるため、定型的なコードが減り、既存のコードがよりシンプルになります。Vuexでは、基本的な状態の更新にはアクションとミューテーションを定義する必要があります。

VuexからPiniaへの移行方法

Piniaに移行することで、シンプルさ、柔軟性、保守性の面で多くのメリットが得られますが、実装を確実に行うためには慎重な計画と準備が必要です。

移行する前に、次のことを確認してください。

  1. PiniaとVuexのアーキテクチャ、状態管理パターン、APIの違いを理解すること:これらの違いを理解することは、コードを効果的にリファクタリングし、Piniaの機能を最大限に活用するために非常に重要です。
  2. Vuexの状態、アクション、ミューテーション、ゲッターを理解し、Piniaの構造に合うようにリファクタリングする:Piniaでは状態を関数として定義することを忘れないようにご注意くださいアクションで状態を直接変更でき、ゲッターで行いたいことスムーズに実装できます。
  3. Vuex storeのモジュールをどのように遷移させるか計画すること:PiniaではVuexと同じようにモジュールを使用することはできませんが、同じような目的でstoreを構成することができます。
  4. 改善の加えられたPiniaのTypeScriptサポートを活用する:プロジェクトでTypeScriptを使用している場合には、Piniaの優れた型推論と型付け機能で、型の安全性と使い勝手を改善するのがおすすめです。
  5. 状態管理の変更に対応するためにテストのしかたを見直す:単体テストや統合テストでstoreやアクションをモックする方法の更新といった操作が必要になるかもしれません。
  6. 移行がプロジェクトの構造や構成にどのような影響を与えるかを考えること:命名規則や、コンポーネント間でstoreをインポートして使用する方法などの要因を考慮しましょう。
  7. 他のライブラリとの互換性を確認する:変更により影響を受ける可能性のある依存関係などを確認する必要があります。
  8. パフォーマンスの変化を評価すること:Piniaは一般的にVuexよりも軽量ですが、問題がないことを確認するために、移行中および移行後もアプリケーションのパフォーマンスを監視してください。

VuexからPiniaへのstoreの変換には、構造やAPIの違いに対応するためにいくつかのステップが必要になります。先ほどのPinia storeの例を考えてみましょう。

同じstoreをVuexのstore.jsファイルに書くと以下のようになります。

import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);

export default new Vuex.Store({
    state: {
        count: 0,
    },
    mutations: {
        increment(state) {
            state.count++;
        },
    },
    actions: {
        increment(context) {
            context.commit('increment');
        },
    },
    getters: {
        doubleCount(state) {
            return state.count * 2;
        },
    },
});

先ほどのPinia storeと同様に、このVuexの例ではstateオブジェクトが含まれており、countプロパティは0で初期化されています。

mutationsオブジェクトには状態を直接変更するメソッドがあり、actionsオブジェクトのメソッドがincrement の変更をコミットします。

そして、gettersオブジェクトがdoubleCountメソッドを保持し、countの状態に2を掛け合わせ、結果を返します。

このコードが示すように、Piniaでstoreを実装する際にはVuexとの顕著な違いがいくつかあります。

  • 初期化:PiniaではVue.use()は必須ではない
  • 構造:Piniaでは、状態はオブジェクトを返す関数であるため、TypeScriptのサポートと反応性が向上する
  • アクション:Piniaのアクションは、ミューテーションを必要とせず状態を直接変えるメソッドであり、コードがシンプルになる
  • ゲッター:Vuexと似ているもののPiniaのゲッターはstore内で定義され、状態に直接アクセスできる

コンポーネントでのstoreの使い方

Vuexでは、storeを次のように使用することができます。

<template>
	<div>{{ doubleCount }}</div>
	<button @click="increment">Increment</button>
</template>

<script>
import { mapGetters, mapActions } from 'vuex';

export default {
	computed: {
	...mapGetters(['doubleCount']),
	},
	methods: {
	...mapActions(['increment']),
	},
};
</script>

Piniaの場合は、以下のようになります。

<template>
	<div>{{ store.doubleCount }}</div>
	<button> @click="store.increment">Increment</button>
</template>

<script>>
import { useStore } from '/src/store';

export default {
	setup() {
	const store = useStore();
	return {
		store,
	};
	},
};
</script>
Pinia Store Demoランディングページのスクリーンショット:(0、2、4 と数値を変更している)
Piniaのstoreを使って値を変更してみた結果

この例では、基本的なパターンを扱いました。より複雑なVuex store、特にモジュールを使用するstoreの変換には、Piniaのアーキテクチャに合わせるためにより細かい再構築が必要になります。

Vueアプリケーションのデプロイ方法

デプロイの実演にそのまま従うには、まずKinstaのアプリケーションホスティングサービスのアカウント作成を行ってください。Dockerファイルを使用してアプリケーションをデプロイします。

プロジェクトのルートにDockerfileを作成し、以下の内容を貼り付けます。

FROM node:latest
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY ./ .
CMD ["npm", "run", "start"]

このコードは、KinstaのDockerエンジンに、Node.jsのインストール(FROM node:latest)、作業ディレクトリの作成(WORKDIR /app)、package.jsonファイルからのnodeモジュールのインストール(RUN npm install)、Vueアプリの起動時に呼び出されるstart (CMD ["npm", "run", "start"]) コマンドの設定を行うように指示するものです。COPYコマンドにより、指定したファイルやディレクトリを作業ディレクトリにコピーします。

その後、好みのGitサービス(BitbucketGitHub、またはGitLab)にコードをプッシュします。リポジトリの準備ができたら、以下の手順に従ってアプリをKinstaにデプロイします。

  1. ログインするか、アカウントを作成してMyKinstaを表示します。
  2. 選択したGitサービスでKinstaを認証します。
  3. 左側のサイドバーで「アプリケーション」を選択し「アプリケーションの追加」ボタンをクリックします。
  4. 表示されるポップアップで、デプロイしたいリポジトリを選択します。複数のブランチがある場合は、希望のブランチを選択し、アプリケーションに名前を付けることができます。
  5. 利用可能なデータセンターの中から一箇所を選択します。
  6. ビルド環境を選択し「Use Dockerfile to set up container image」を選択します。
  7. Dockerfileがリポジトリのルートにない場合は、「Context」でパスを指定し、「Continue」をクリックします。
  8. Startコマンド」の項目は空欄のままで構いません。Kinstaのシステムがnpm startを検知しアプリケーションを起動します。
  9. アプリケーションに見合ったPodサイズとインスタンス数を選択し「続行」をクリックします。
  10. クレジットカード情報を入力し「アプリケーションの作成」をクリックします。

アプリケーションホスティングの代わりに、Kinstaの静的サイトホスティングを利用して、Vueアプリケーションを静的サイトとして無料でデプロイすることも可能です。

まとめ

VuexからPiniaへの移行は、Vueエコシステム内の状態管理に大きな進化をもたらしました。Piniaのシンプルさ、改善の施されたTypeScriptサポート、Vue 3のComposition APIとの連携により、次世代のVueアプリケーションにとって魅力的な選択肢となっています。

Kinstaを使うことで、Vueアプリケーションを高速、安全、信頼性の高いインフラストラクチャで管理することができます。早速、Kinstaでアカウントを作成アプリケーションホスティングをお試しください。

Jeremy Holcombe Kinsta

Kinstaのコンテンツ&マーケティングエディター、WordPress開発者、コンテンツライター。WordPress以外の趣味は、ビーチでのんびりすること、ゴルフ、映画。高身長が特徴。