31
23

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Piniaの基本的なつかいかた

Last updated at Posted at 2022-07-22

Piniaとは

PiniaはVuejs向けの状態管理ライブラリだ。コンポーネント間やページ間でグローバルに状態共有することができる。
VuejsのComposition APIを意識して実装されているものの、OptionsAPIやVue2に適用することも可能だ。
Vuex5.xのRFCの要求事項をほとんど満たしていたということもあって、Vuexに代わってVue公式の状態管理ライブラリに指名されている。すなわち、比較的新しいライブラリながらも本番案件に利用できるだけの機能が十分に備わっているといえる。
この記事では取り上げないが、サーバーサイドレンダリング(ViteやNuxt.js)にも対応している。
ちなみに、発音は「ピーニャ」だ(発音記号で書けば/piːnjʌ/)。

この記事で解説すること

Pinia公式ドキュメントに倣いつつ、以下の項目について解説する。

  • インストール方法
  • 簡単な利用例
  • Store、State、Getters、Actionsの詳しい使い方

インストール

以下コマンドで導入可能。

yarn add pinia
npm install --dev pinia

試してみる

PiniaをVueアプリインスタンスに適用

以下のように、PiniaインスタンスをApp.use()の引数に入れればVueアプリでPiniaが使用可能になる。

main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const pinia = createPinia()
createApp(App).use(pinia).mount('#app')

Storeの作成

まずはdefineStore()を使用してStoreを作成する。
defineStore()の第一引数にはStoreの一意なIDを付け、第二引数にStoreの定義を書く。
そしてdefineStore()の戻り値をエクスポートし、他のモジュールで使うことになる。

myStore.ts
import { defineStore } from 'pinia'

export const useMyStore = defineStore('myStore', {
  //保持したいデータ
  state: () => {
    return {
      count: 0,
    }
  },
  getters: {
    double: (state) => {
      return state.count * 2
    },
  },
  actions: {
    increment() {
      this.count++
    },
  },
})

PiniaのStoreには

  • state
  • getters
  • actions

の3つのプロパティを定義できる。
stateにはデータの読み書き、gettersには加工したデータの読み取り、actionsには各種メソッドを実装していく。
これらはVuejsのdata, computed, methodsに相当するものと考えればわかりやすい。
他にもwatchに相当するものとして、Stateの変更Actionsの呼び出しの監視機能がある。

コンポーネントからの利用

PiniaのStoreをコンポーネントで使う場合の例が以下になる。
記述量が減るので、<script setup>での利用を推奨。

MyComponent.vue
<template>
  <div class="hello">
    <button @click="myStore.increment">increment</button>
    <p>Count: {{ myStore.count }}</p>
    <p>DoubleCount: {{ myStore.double }}</p>
    <p>Count(String): {{ countString }}</p>
  </div>
</template>

<script lang="ts" setup>
  import { computed } from 'vue'
  import { useMyStore } from '@/myStore'
  const myStore = useMyStore()
  const countString = computed(() => {
    return String(myStore.count)
  })
</script>

script setupを利用しない場合は以下のようになる。
注意点として、useMyStore()setup()関数内で実行しなくてはならない。
理由は後述の「コンポーネント外でStoreを使う」を参照のこと。

MyComponent.vue
<template>
  <div class="hello">
    <button @click="myStore.increment">increment</button>
    <p>Count: {{ myStore.count }}</p>
    <p>DoubleCount: {{ myStore.double }}</p>
    <p>Count(String): {{ countString }}</p>
  </div>
</template>

<script lang="ts">
  import { computed, defineComponent } from 'vue'
  import { useMyStore } from '@/myStore'

  export default defineComponent({
    setup() {
      const myStore = useMyStore()
      const countString = computed(() => {
        return String(myStore.count)
      })
      return {
        computed,
        myStore,
        countString,
      }
    },
  })
</script>

コンポーネントからStoreを利用するには、まず先ほどエクスポートしたuseXXXをインポートし、関数として実行した戻り値をStoreとして扱う。

import { useMyStore } from '@/myStore'
const myStore = useMyStore()

あとはこのStoreに登録された各stateなどを呼び出せばよい。

SomeComponent.vue
// stateの読み取り
const count = myStore.count

// stateの更新
myStore.count = 5

// gettersにアクセス
const doubledCount = myStore.double

// actionsを実行
myStore.increment()

当然ながら、違うファイル内でも同じStoreを呼び出せば値は共有される。

AnotherComponent.vue
import { useMyStore } from '@/myStore'
const myStore = useMyStore()
console.log(myStore.count) // 5

Vuexと比べると、stateの読み取りに.stateを挟む必要がない、store.commit()無しで状態更新ができるなど、構文が短くなっている。
以上のように、かなりスッキリとした構文でStoreを利用することができる。

なお、非同期処理関数を使いたい場合には後述のようにActionsで実装することになる。Gettersには非同期処理関数を登録できないので注意。

より詳しい使い方

Store

命名規則

defineStoreの戻り値の関数はComposableな関数にあたる。
CompositionAPIの慣習に則り、Pinia公式では戻り値の名前をuseXXXの形式にすることを推奨している。
(参考:https://pinia.vuejs.org/core-concepts/)

コンポーネント外でStoreを使う

PiniaのStoreはPiniaインスタンスに依存している。
そのため、Piniaインスタンスが準備されていない環境でuseMyStoreなどを呼び出すとエラーになる。

useMyStore()
// getActivePinia was called with no active Pinia. Did you forget to install pinia?
//   const pinia = createPinia()
//   app.use(pinia)

Storeのテストを行うなど、Component外でStoreを利用したい場合は、useMyStore()にPiniaインスタンスを注入すればよい。
方法としてはPiniaインスタンスを作って注入する方法と、setActivePinia()を使う方法の2通りがある。

import { setActivePinia, createPinia } from 'pinia'
import { useMyStore } from './myStore'

// 方法1:Piniaインスタンスを作って普通に注入
const pinia = createPinia()
const myStore = useMyStore(pinia)

// 方法2:setActivePinia()を利用
setActivePinia(createPinia())
const myStore = useMyStore()

複数のStore

Piniaで複数のStoreを使いたい場合は、それぞれを別のファイルに定義する。
Store間で連携が必要になった場合でも、useXXX()でStoreを呼び出せばそのstateなどを利用できる。
とはいえ、2つのStoreが相互に参照しあうようであれば1つに統合した方がよい。

以下は、myStoreの他にanotherStoreを作成し、その中でmyStoreのstateを呼び出す例。

anotherStore.ts
import { defineStore } from 'pinia'
import { useMyStore } from './myStore'

export const useAnotherStore = defineStore('anotherStore', {
  state: () => {
    return {
      text: '',
    }
  },
  getters: {
    textPlusCount: (state) => {
      const myStore = useMyStore()
      // myStoreのstate呼び出し
      return state.text + myStore.count

      // getters呼び出し
      // myStore.double

      // actions呼び出し
      // myStore.increment()
    },
  },
})

リアクティブ性を保ちつつスプレッド構文を使う

PiniaのStoreはreactiveでラップされたオブジェクトなので、スプレッド構文で展開すると、StateやGettersのリアクティブ性を壊してしまう。

const myStore = useMyStore()
// ❌ リアクティブ性が壊れてしまう = 変更を検知できない
const { name, doubleCount } = myStore

リアクティブ性を保つには、storeToRefs()を用いるとよい。
なお、actionsはそのままスプレッド構文を用いても問題ない。

import { storeToRefs } from 'pinia'

const store = useMyStore()
// リアクティブな参照になる
const { name, doubleCount } = storeToRefs(myStore)

// actionsはそのままスプレッド構文を用いてもOK
const { increment } = myStore
})

State

Stateへのアクセス

Storeに定義したStateは、基本的に直接アクセス可能。例えば、下記のように加算演算子を使っても問題なく動作する。

myStore.counter++

Stateのリセット

store.$reset()を呼び出すことで、Stateを定義した初期状態にリセットすることができる。

myStore.count = 100

// ...

// countの値が、Store定義時にStateとして設定した値に戻る。
// この場合は0になる。
myStore.$reset()

Stateの変更

Stateを変更する際、Piniaではstore.counter = 1store.counter++のように直接操作できることを既に学んだ。
この他に、store.$patchメソッドを呼んで変更する方法をとることもできる。
このメソッドは、変更後のStateを直接引数に入れるのと、stateを引数にとるコールバックを引数に入れる使い方の2通りが用意されている。
基本的には前者でよいが、配列の操作など変更内容が複雑な場合には後者を使うとよい。

// オブジェクトを代入
myStore.$patch({
  count: myStore.count + 100
})

// コールバック
myStore.$patch((state) => {
  state.count = state.count + 100
})

State全体の置き換え

あるStoreのState全体を置き換える操作は、store.$stateに直接代入することで実行できる。

myStore.$state = {
  count: 24,
  isCountStop: true,
}

さらに、Piniaインスタンスが持つすべてのStateを置き換えるには次のようにすればよい。
(サーバーサイドレンダリングのState hydrationに使うらしい)

pinia.state.value = {}

Stateの変更監視

$subscribe()メソッドを使うことで、Stateに変更があった場合に行いたい処理を設定できる。引数にはmutationstateを引数にとるコールバックを登録する。

  • mutation.typeは、前述のStateの変更時に使った方法によって異なる値になる。直接代入なら'direct'$patch()の引数にオブジェクトを入れれば'patch object'、関数を入れれば'patch function'になる。
  • mutation.storeIdは、Storeの定義時に設定したStoreのIDを格納している。
  • mutation.payloadmutation.type'patch object'の時にのみ、$patch()の引数に代入したオブジェクトを格納している。
// mutation.typeの型
// import { MutationType } from 'pinia'
myStore.$subscribe((mutation, state) => {
  console.log(mutation.type) // 'direct' | 'patch object' | 'patch function'
  // myStore.$idと同じもの
  console.log((mutation.storeId) // 'myStore'
  // mutation.type === 'patch object'の時のみ有効
  console.log(mutation.payload) // myStore.$patch()に渡したオブジェクト

  // Stateに変更があったときに実行したい処理を書いていく
  // ...
})

Getters

同じStore内の他のGettersにアクセス

同じStore内の他のGettersの戻り値を参照したい場合は、thisを使ってアクセスする。
その場合

  • アロー関数は使うことができない(thisundefinedになるため)
  • stateにアクセスしない場合、引数のstateも取り除く(linter設定しだいではエラーになる)

に気を付けたい。

export const useMyStore = defineStore('myStore', {
  state: () => {
    return {
      count: 0,
    }
  },
  getters: {
    double: (state) => {
      return state.count * 2
    },
    // アロー関数はthisがundefinedになる可能性があるため使えない
    doublePlusOne(): number {
      // thisでアクセスする
      return this.double + 1
    },
  },
})

Gettersに引数を渡す

Gettersに引数を渡したい場合、Gettersの戻り値を値ではなく関数にする。
その関数の引数には、Gettersに渡したい引数を設定する。

export const useMyStore = defineStore('myStore', {
  state: () => {
    return {
      count: 0,
    }
  },
  getters: {
    double: (state) => {
      return state.count * 2
    },
    multiplied: (state) => {
      // 関数を返すようにする
      return (num: number) => state.count * num
    }
  },
})

呼び出し側では通常の関数のように利用できる。

// 呼び出し
myStore.multiplied(5)

Actions

非同期処理を行うActions

PiniaのActionsには、非同期処理を実行する関数も登録できる。
通常の非同期処理関数のようにasyncキーワードを使えばよい。

export const useMyStore = defineStore('myStore', {
  state: () => {
    return {
      count: 0,
    }
  },
  actions: {
    async fetchCount(){
      const fetchedCount = await fetchXxxValue()
      this.count = fetchedCount 
    }
  },
})

Actionsの呼び出しを監視する

$onActionメソッドを使うと、PiniaのストアのActionsが実行されるタイミングで別の処理を実行させることができる。
$onActionの引数に実行したい処理を行うコールバックを代入する。
処理を実行させたいタイミング(action実行前、実行後、例外発生時)に応じて、次の3か所に処理を実装していくことになる。下記のサンプルコードも参照のこと。

  • action実行前: $onActionに渡すコールバック内
  • action実行後: afterに渡すコールバック内
  • 例外発生時: onErrorに渡すコールバック内
myStore.$onAction(
  ({
    name, // 実行されたactionの名前
    store, // Storeのインスタンス。`myStore`に同じ
    args, // actionに渡された引数の配列
    after, // actionの完了後(Promiseであれば`resolve()`された後)に実行する処理
    onError, // actionで例外が投げられたとき(Promiseであれば`reject()`されたとき)に実行する処理
  }) => {
    // action実行前に行われる処理
    console.log({
      name,
      store,
      args,
    })
    
    after((result) => {
      // action実行後に行われる処理
      console.log(result)
    })

    onError((error) => {
      // actionでエラーが発生したときに行われる処理
      console.error(error)
    })
  }
)

なお、この機能ではStoreのActions実行を監視できるが、コールバックはStore内のすべてのActionsに対して実行される。すなわち、個別のactionのみを直接監視することはできない。
その場合は以下のようにaction実行前に行う処理の中でactionの名前(name)をチェックし、後続処理を続けるか分岐させるとよい。

// action実行前に行われる処理
// `someAction`以外は無視
if(name !== 'someAction') return

さらなる機能として、上記で登録したコールバックの実行を止めさせることもできる。
そのためには$onActionの戻り値を受け取っておき、止めさせたいタイミングで実行する。

const unsubscribe = myStore.$onAction(/* 略 */)

// 上で登録したコールバックを登録解除する
unsubscribe()

まとめ

Vuejsの新たな状態管理ライブラリであるPiniaについて、以下の点を紹介した。

  • インストール方法
  • 簡単な利用例
  • Store、State、Getters、Actionsの詳しい使い方

この記事を参考にしながら、ぜひPiniaを使ってみてほしい。

参考

Pinia公式ガイド https://pinia.vuejs.org/introduction.html
公式ガイドにはこの記事では紹介しなかったトピック(サーバーサイドレンダリング対応、Vuex≦4からの移行など)も紹介されているため、必要になった場合は参照してほしい。

31
23
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
23

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?