Lesson 8

状態管理

Lesson 8 Chapter 1
状態管理について

1. 状態管理とは

Web アプリケーションでは、様々なデータを使用して HTML ページを表示します。
サーバから取得したデータで画面表示を行うのみでなく、ユーザーが入力した内容や、ログイン/ログアウトの状態によって表示内容を切り替えたりすることも必要になります。

これらのデータが複数のコンポーネントで共通に使用される場合には、データ状態を共有しつつ、相互に矛盾なく更新保持する必要が生じます。

このようなアプリケーションの保持するデータの管理を「状態管理」と呼んでいます。
フロントエンド開発においては、この状態管理を適切に行うことが重要となります。

2. 状態管理の方法

2-1. コンポーネント間のデータの受渡し

Lesson 6 で学習しましたように、Vue には、コンポーネント間でデータの受渡しを行う方法がいくつか用意されています(例えば次のようなもの)。

No 機能 簡単な説明
1 props 親コンポーネントから子コンポーネントに値を渡す。
2 emit 子コンポーネントから親コンポーネントにイベントを渡す。
3 状態管理機能 Pinia や vuex などのライブラリでコンポーネント間でデータを共有。
4 provide / inject 親コンポーネントから子コンポーネントにデータやロジックを渡す。

規模の小さなアプリケーションの場合は、props や emit だけで構成するという選択肢もありますが、規模の大きなアプリケーションの場合は、保有する状態の数やその組合せも多くなるため、コンポーネント内で個別にデータを管理するのは困難となります。

2-2. 状態管理ライブラリ

そこで、中規模以上の開発では「状態管理ライブラリ」というものを使用して、コンポーネントから独立してグローバルに状態(データ)を管理することが一般的です。

Vue で使用できる状態管理ライブラリとして「Vuex(ビューエックス)」と「Pinia(ピニア)」 があります。
元々は、Vuex が Vue の公式ライブラリでしたが、現在はその後継(※)となる Pinia が Vue の公式ライブラリとなっているため、このカリキュラムでは Pinia を使用して学習を進めていきます。

Pinia と Vuex 5

Pinia は「Composition API に対応する状態管理ストアを再設計する実験」として 2019 年に開発が始まりました。
一方、Vuex は、従来の設計を大幅に見直す新バージョン 5(Vuex 5)の開発を進めていましたが、Pinia の開発者も Vuex 5 のコアチームのメンバーに所属しており、Vuex 5 と Pinia は相互に影響を受けつつ開発が進められました。

結果として Pinia には、Vuex 5 とほぼ同じか拡張された API が実装されることになり、最終的に Vue の公式ライブラリとして採用されました(Vuex 5 はリリースされませんでした)。

次のように、Vuex の公式ページにおいては「Pinia の使用を強く推奨」するとともに「Pinia は Vuex 5 の別名と考えてよい」と紹介されています。

vue_8-1-1.png

3. Pinia について

3-1. Pinia の公式ページ

先に触れましたとおり、Pinia(ピニア)は Vue 公式の状態管理ライブラリです。
Pinia には、以下のような公式ページが用意されています。

vue_8-1-2.png

3-2. Pinia の機能

Pinia がどういう機能を提供するかを簡単に説明しておきます。

下図のように、Pinia はコンポーネント間の共通データを「State」(※)に保持します。
この State は、外部から書き換えることはできず「Actions」に定義されたメソッドでのみ書き換えることができます。
また、State を外部から取得するためには「Getters」というメソッドを使用します。

vue_8-1-3.png

以上のような State、Actions、Getters を提供する機能群を一般に「Store(ストア)」といい、Pinia は、この Store の部分を提供するライブラリとなります。

State Management

状態管理とは、英語の「State Management」を日本語に訳したものです。
State(状態)はアプリケーションの保持しているデータを指し、これを Management(管理)するために、Pinia などのライブラリを使用することになります。

Lesson 8 Chapter 2
Pinia の導入

1. Pinia のインストール

1-1. インストール方法の確認

それでは、Vite プロジェクトに Pinia をインストールしていきます。
インストール方法は、公式ページに記載されています。

vue_8-2-1.png

npm を使用する場合のコマンドは、次のところです。

npm install pinia

1-2. 既存のプロジェクトにインストールする

ここでは、Lesson 7 で作成した vite-sample2 プロジェクトにコードを追加していきます。
ターミナルを開いて、次のように npm install pinia というコマンドを実行しましょう。

vue_8-2-2.png

インストールが成功すると package.json"pinia": "^2.XX.XX" のように、pinia のライブラリが追加されているはずです。

vue_8-2-3.png

インストール自体はこれで完了です。
以下、初期設定に必要なコードを追加していきます。

2. Pinia の設定と実装

プロジェクトで Pinia が使用できるようにコードを追加していきます。

2-1. Pinia をプラグインとして追加する

Pinia のインスタンスを Vue のプラグインとして追加する方法は、公式サイトで以下のように紹介されています。

vue_8-2-4.png

Vue Router のときと同様に、プラグインの追加には use() 関数を使用します。

上の例にならって、main.js ファイルを以下のように修正します。
多少書き方は異なりますが、指定している内容は全く同じです。

vue_8-2-5.png

全てのコードをテキストで表示
src\main.js
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router/index'
import { createPinia } from 'pinia'

const pinia = createPinia()

createApp(App)
.use(router)
.use(pinia)
.mount('#app')

以上で、プロジェクト内で Pinia が使用できるようになりました。
続いて、Store を定義して、コンポーネントから操作できるようにしていきます。

2-2. Store を作成する

① 公式の記載例

まず、Store を 1 つ作成します。ここでは、以下の公式ページに紹介されている Store を参考にして Store の定義を行います。

vue_8-2-6.png

この例は、シンプルではありますが、state、getters、actions の 3 つの基本機能が定義されています。なお、見出しに「Option Store」とあるように、Options API と同じような書き方になっています。

② コードの記載

それでは、実際にコードを書いていきましょう。
次のように、src ディレクトリの下に stores ディレクトリを追加した上で、counter.js ファイルを追加してください。

vue_8-2-7.png

counter.js ファイルに記述したコードは次のとおりです。
state: の書き方のみ、公式の例から少し変更しています(もちろん、公式のとおりに書いても OK です)。

src\stores\counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => {
    return {
      count: 0,
      name: 'Eduardo'
    }
  },
  getters: {
    doubleCount: (state) => state.count * 2,
  },
  actions: {
    increment() {
      this.count++
    },
  },
})

以下、記載している内容について見ていきます。

③ 記載したコードの確認

Store は、defineStore 関数を使用して定義します。

import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', { オプションを指定 })

defineStore 関数の第 1 引数には、この Store を識別する Id を記載します。
Id 名は何でも良いのですが、ファイル名と一致させるのが一般的です(ここでは 'counter' と定義)。

defineStore 関数の第 2 引数にはオプション(options)を定義します。
定義できるオプションは、次のとおりです。

No option 説明
1 state 保持する共通データを定義する(リアクティブ変数に相当)
2 getters state からデータを取得するための getter を定義する(算出プロパティ computed に相当)
3 actions state に保存されているデータを更新するメソッドを定義する(メソッドに相当)

・state の定義
state には、以下のような形でリアクティブな変数を定義します。
ここでは、countname という 2 つの変数を定義しています。

  state: () => {
    return {
      count: 0,
      name: 'Eduardo'
    }
  },

・getters の定義
getters には Store のデータを取得するための getter 関数を定義します。
この getter 関数の第 1 引数で、state を受け取ります。
ここでは、state.count * 2 として、count 変数を 2 倍した値を戻り値としています。

  getters: {
    doubleCount: (state) => state.count * 2,
  },

・actions の定義
actions には、state の値を更新するためのメソッドなどを記入します。
ここでは、count 変数に 1 を加算する increment メソッドを定義しています。

  actions: {
    increment() {
      this.count++
    },
  },

2-3. コンポーネントで Store を利用する

次に、作成した Store をコンポーネント側で呼び出して使用するようにします。

① 公式の記載例

setup() 関数から Store を呼び出す方法は、以下の公式ページに紹介されています。

vue_8-2-8.png

Store の定義ファイル counter.js から useCounterStore をインポートして、setup() 構文内でインスタンス化しています(const store = useCounterStore() のところ)。
このインスタンス store を使用して、counter ストアの操作を行うことになります。

② コードの記載

それでは、手元のプロジェクトにコードを追加していきます。
src\components\Home.vue コンポーネントを次のように修正してください(赤枠部分)。

vue_8-2-9.png

修正後の Home.vue コンポーネントは次のとおりです。

src\components\Home.vue
<script setup>
import { useCounterStore } from '../stores/counter'
const store = useCounterStore()
</script>

<template>
  <div>Home</div>
  <button @click="store.increment">加算</button>
  <div>name: {{ store.name }}</div>
  <div>count: {{ store.count }}</div>
  <div>doubleCount: {{ store.doubleCount }}</div>
</template>

以下、記載している内容について見ていきます。

③ 記載したコードの確認

次のところで、counter.js ストアから useCounterStore をインポートして、store という名称のインスタンスを生成しています(名称は何でも構いません)。

import { useCounterStore } from '../stores/counter'
const store = useCounterStore()

・state の値を取得
次のところで、Store 内で定義したリアクティブ変数 namecount を表示しています。

  <div>name: {{ store.name }}</div>
  <div>count: {{ store.count }}</div>

単純に、store.リアクティブ変数名 という形式で取得できます。

・getters の値を取得
getters で定義した doubleCount も、state と同様の形式で取得・表示できます。

  <div>doubleCount: {{ store.doubleCount }}</div>

・actions を実行
Store で定義した actions(increment)も、以下のように、store.アクション名 という形式で使用することができます。

  <button @click="store.increment">加算</button>

2-4. ブラウザで動作確認

それでは、VSCode のターミナルを開き、npm run dev コマンドでローカルサーバを立ち上げます。
ブラウザから http://localhost:5173/ にアクセスして画面を表示してください(ローカルサーバを複数立ち上げている場合などは URL が異なる場合があります)。

下図の「加算」ボタンは、actions(increment)を実行するものです。
name と count は、ストアの state を表示する部分で、それぞれ初期値が表示されています。
doubleCount には、getters(doubleCount)の値が標示されます。

vue_8-2-10.png

それでは「加算」ボタンをクリックしてみましょう。
クリックする度に、値がリアクティブに更新されます。
以下は、3 回クリックした場合の画面です。

vue_8-2-11.png

以上で、pinia の導入は完了です。
次のチャプターから、pinia の使用方法について詳しく見ていきます。

Lesson 8 Chapter 3
Store(ストア)の定義

このチャプターでは、Store(ストア)の定義方法について見ていきます。

1. 構文(Option Stores と Setup Stores)

Store の記述方法は「Option Stores」と「Setup Stores」の 2 つの形式から選択することができます。

① Option Stores

先ほどは、以下のような「Option Stores」形式を使用しました。
state:getters:actions: という 3 つのオプションを個別に定義しています。

export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0, name: 'Eduardo' }),
  getters: {
    doubleCount: (state) => state.count * 2,
  },
  actions: {
    increment() {
      this.count++
    },
  },
})

② Setup Stores

もう一つの構文として「Setup Stores」というものがあります。
これは、次のように、setup() 構文と同じような書き方となります。

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const name = ref('Eduardo')
  const doubleCount = computed(() => count.value * 2)
  function increment() {
    count.value++
  }

  return { count, name, doubleCount, increment }
})

下表のとおり、state は ref 関数で定義し、getters は computed(算出プロパティ)で定義し、actions はメソッドとして定義します。

No Option Stores Setup Stores
1 state に定義 ref 関数を使用して定義
2 getters に定義 算出プロパティ computed を使用して定義
3 actions に定義 function(メソッド)で定義

Option Stores と Setup Stores のどちらを使用するか

Pinia 公式では、Option Stores と Setup Stores は「使いやすい方」を選択すればよく、迷う場合は「Option Stores」から試してくださいとしています(What syntax should I pick?)。

なお、これら 2 つの構文については、Options API と Composition API のような実利的な違いはないようです。
Pinia 公式ページのコード例では Option Stores を使用しているものが多いことから、本レッスンでは、主に Option Stores の構文を使用して学習を進めていきます。

2. Setup Stores の動作確認

現在記述しているコードを Setup Stores の形式で書き直して動作確認を行います。
なお、本レッスンでは、ベースとして Option Stores を採用しますので、動作確認後に元の状態に戻します。

2-1. Setup Stores 構文でコードを記述

それでは、実際にコードを書いていきましょう。
counter.js ファイルを次のように修正します。

vue_8-3-1.png

全てのコードをテキストで表示
src\stores\counter.js
// import { defineStore } from 'pinia'

// export const useCounterStore = defineStore('counter', {
//   state: () => {
//     return {
//       count: 0,
//       name: 'Eduardo'
//     }
//   },
//   getters: {
//     doubleCount: (state) => state.count * 2,
//   },
//   actions: {
//     increment() {
//       this.count++
//     },
//   },
// })

import { ref, computed } from 'vue'
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const name = ref('Eduardo')
  const doubleCount = computed(() => count.value * 2)
  function increment() {
    count.value++
  }

  return { count, name, doubleCount, increment }
})

Option Stores で記述していた部分は、後で復活させるためコメントアウトとしておいてください。
新たにしたコードは次のとおりです。

src\stores\counter.js
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const name = ref('Eduardo')
  const doubleCount = computed(() => count.value * 2)
  function increment() {
    count.value++
  }

  return { count, name, doubleCount, increment }
})

先に記載した構文例と同じのため、解説は省略します。

2-2. ブラウザで動作確認

それでは、ブラウザで開いて動作確認をしてみましょう。
「加算」ボタンをクリックして、次のように正しく値が変更されていれば OK です。

vue_8-3-2.png

2-3. コードを元に戻す

確認が終わりましたら、コードを元に戻します。
次のように、Option Stores の構文を復活させ、Setup Stores の記述を削除(またはコメントアウト)してください。

vue_8-3-3.png

全てのコードをテキストで表示
src\stores\counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => {
    return {
      count: 0,
      name: 'Eduardo'
    }
  },
  getters: {
    doubleCount: (state) => state.count * 2,
  },
  actions: {
    increment() {
      this.count++
    },
  },
})

// import { ref, computed } from 'vue'
// import { defineStore } from 'pinia'

// export const useCounterStore = defineStore('counter', () => {
//   const count = ref(0)
//   const name = ref('Eduardo')
//   const doubleCount = computed(() => count.value * 2)
//   function increment() {
//     count.value++
//   }

//   return { count, name, doubleCount, increment }
// })

3. Store のデータをプロパティごとに分割する方法

3-1. state をプロパティごとに分割する構文

先に記述したコードでは、次のように store のインスタンスを取得して、そのまま store をテンプレートで使用していました。

script

import { useCounterStore } from '../stores/counter'
const store = useCounterStore()

template(store を介してプロパティを取得)

<div>count: {{ store.count }}</div>
<div>doubleCount: {{ store.doubleCount }}</div>

ここでは、次のようにインスタンスをプロパティに分割して、取得する方法について確認していきます。

const { count, doubleCount } = store // × この方式では分割取得できない

なお、上記のような一般的な記述では、取得したプロパティのリアクティブ性は失われてしまいます。

リアクティブ性を保持したままプロパティを分割するには、以下のように storeToRefs() 関数というものを使用する必要があります。

script

import { useCounterStore } from '../stores/counter'
import { storeToRefs } from 'pinia'
const store = useCounterStore()
const { count, doubleCount } = storeToRefs(store)

template

<div>count: {{ count }}</div>
<div>doubleCount: {{ doubleCount }}</div>

3-2. 具体例を使用した動作確認

① コードの修正

src\components\Home.vue コンポーネントに次の赤枠部分を追加します。
本来は storeToRefs() 関数を使用すべきところですが、比較のため、最初は「一般的なオブジェクトの分割記法」を使用しています。

vue_8-3-4.png

全てのコードをテキストで表示
src\components\Home.vue
<script setup>
import { useCounterStore } from '../stores/counter'
const store = useCounterStore()
const { count, doubleCount } = store
</script>

<template>
  <div>Home</div>
  <button @click="store.increment">加算</button>
  <div>name: {{ store.name }}</div>
  <div>count: {{ store.count }}</div>
  <div>doubleCount: {{ store.doubleCount }}</div>
  <div>分割 count: {{ count }}</div>
  <div>分割 doubleCount: {{ doubleCount }}</div>
</template>

② ブラウザで動作確認

ブラウザで開いて「加算」ボタンを押してみましょう。
新たに追加した「分割 count:」および「分割 doubleCount:」の値が初期値のまま変わらないことが確認できます。 つまり、リアクティブ化されていないということになります。

vue_8-3-5.png

これをリアクティブにするには、以下のように storeToRefs() 関数を使用します。

③ コードの追加修正

Home.vue コンポーネントを storeToRefs() 関数を使用して、次のように修正します。

vue_8-3-6.png

全てのコードをテキストで表示
src\stores\counter.js
<script setup>
import { useCounterStore } from '../stores/counter'
import { storeToRefs } from 'pinia'
const store = useCounterStore()
const { count, doubleCount } = storeToRefs(store)
</script>

<template>
  <div>Home</div>
  <button @click="store.increment">加算</button>
  <div>name: {{ store.name }}</div>
  <div>count: {{ store.count }}</div>
  <div>doubleCount: {{ store.doubleCount }}</div>
  <div>分割 count: {{ count }}</div>
  <div>分割 doubleCount: {{ doubleCount }}</div>
</template>

④ 修正後の動作確認

再びブラウザで開いて「加算」ボタンを押してみましょう。
今度は、クリックするたびに、「分割 count:」および「分割 doubleCount:」の値が連動して加算することが確認できると思います。

vue_8-3-7.png

3-3. actions は直接分解が可能

なお、リアクティブ変数でない actions(メソッド)は、次のように分割取得することが可能です。

const { increment } = store

ここでは、特に動作確認をしませんが、興味があればご自身で確認をしていただければと思います。

4. 複数の Store を定義する

次に、Store を複数定義する方法です。
一般的には、役割ごとに Store を作成して状態(state)を管理することになります。

方法は簡単で、src\stores ディレクトリ内に、Store ファイルを追加するだけで OK です。
早速ですが、プロジェクトに Store を追加してみましょう。

4-1. user ストアを追加

以下のように、src\stores ディレクトリに、user.js ファイルを追加します。

vue_8-3-8.png

記述したコードは次のとおりです。

src\stores\user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => {
    return {
      id: 1
    }
  }
})
Setup Stores 構文で記述した場合

参考までに、Setup Stores 構文で記述した場合は次のようになります。

src\stores\user.js
import { ref } from 'vue'
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', () => {
  const id = ref(1)
  return { id }
})

state のみの簡単な構成で、id の初期値は 1 としています。
以上で、Store の追加については完了です。

4-2. App.vue コンポーネントの修正

新しく作成した user ストアを使用してみましょう。
src\App.vue コンポーネントのうち、userId を定義していた部分(緑枠部分)を削除して、代わりに user ストアの id を当てるようにします。
追加・修正するのは赤枠の部分となります。

vue_8-3-9.png

全てのコードをテキストで表示
src\stores\user.js
<script setup>
import { useUserStore } from './stores/user'
const userStore = useUserStore()
</script>

<template>
  <div>ユーザーID: <input v-model="userStore.id"/></div>
  <p>
    <router-link to="/">Home</router-link> | 
    <router-link to="/about">About</router-link> | 
    <router-link :to="`/user/${userStore.id}`">UserPage</router-link>
  </p>
  <router-view></router-view>
</template>

<style scoped>
</style>

上記のように、Store の state(ここでは id)を、v-model を使用して双方向データバインディングとすることが可能です(※)。

Pinia における双方向データバインディング(v-model)

Store の state(データ)を更新するには、一般的には actions を使用することになります。
しかし、Pinia では、state のプロパティを、v-model で紐づけて双方向データバインディングをすることを許容しています(GitHub のディスカッション参照)。
actions を介すかどうかは、実装者側で判断することになります。

4-3. ブラウザで動作確認

正しく動作するか、ブラウザで確認しておきましょう。
次のように、ユーザー ID 欄に 3 と入力の上「UserPage」をクリックします。

vue_8-3-10.png

user ストアの id との双方向バインディングができていれば、以上のように、正しくページ遷移できるはずです。

Lesson 8 Chapter 4
State(ステート)

1. 基本的な使用方法

1-1. State の定義

State(ステート)は、Store の核となる部分であり、状態(データ)を保持する関数として定義されます。

① Option Stores 構文

Option Stores 構文では、以下のように定義します。

import { defineStore } from 'pinia'

export const useStore = defineStore('storeId', {
  // 型推論のためにアロー関数が推奨される
  state: () => {
    return {
      // プロパティの型は自動的に型推論される
      count: 0,
      name: 'Eduardo',
      isAdmin: true,
      items: [],
      hasChanged: true,
    }
  },
})

型推論(※)を正しく効かせるためには、上記のようにアロー関数の形式で定義することが推奨されています。

型推論

型推論とは、変数などに明示的にデータ型を指定しなくとも、コンパイラなどが自動的にそのデータ型を決定する機能のことをいいます。
Pinia は Vuex と比較してこの型推論の機能が充実しており、TypeScript を使用した Vue の開発と相性が良いこともその特長となっています。

② Setup Stores 構文

Setup Stores 構文を使用して State を定義する場合は、次のように ref 関数を使用します。

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const name = ref('Eduardo')
  const isAdmin = ref(true)
  const items = ref([])
  const hasChanged = ref(true)

  return { count, name, isAdmin, items, hasChanged }
})

1-2. コンポーネントから State へのアクセス

次に、コンポーネントから State を使用する方法です。
これまでの例でも見てきましたが、Store のインスタンス(ここでは userStore)を介して State にアクセスすることで、状態を直接読み書きすることができます。

<script setup>
import { useUserStore } from './stores/user'
const userStore = useUserStore()
userStore.id++
</script>

<template>
  <div>ユーザーID: <input v-model="userStore.id"/></div>
</template>

2. State のメソッド

State には、以下のようなメソッドが用意されています。
これにより、State の内容を初期状態に戻したり、指定した値で更新することができます。

No メソッド 説明
1 $reset() State を初期値にリセット
2 $patch() State を指定した内容で更新
3 $state() State を交換する(内部で $patch() が呼び出される)

2-1. $reset() メソッド

① 基本構文

$reset() メソッドの基本構文は次のとおりです。

const store = useStore()

store.$reset()

② コードの修正

実際にコードを記載して動作確認をしてみましょう。
src\components\Home.vue コンポーネントに赤枠部分の 1 行を追加します。
コードの整理のため、緑枠の部分は削除しておきます(コメントアウトでも OK です)。

vue_8-4-1.png

全てのコードをテキストで表示
src\components\Home.vue
<script setup>
import { useCounterStore } from '../stores/counter'
const store = useCounterStore()
</script>

<template>
  <div>Home</div>
  <button @click="store.increment">加算</button>
  <button @click="store.$reset()">リセット</button>
  <div>name: {{ store.name }}</div>
  <div>count: {{ store.count }}</div>
  <div>doubleCount: {{ store.doubleCount }}</div>
</template>

追加した 1 行は、次の部分です。

<button @click="store.$reset()">リセット</button>

このリセットボタンを押すと、$reset() メソッドが実行され、state の値が初期値にリセットされます。

③ 動作確認

ブラウザで開いて、まず「加算」ボタンを何回か押して count の値を変化させます。

vue_8-4-2.png

次に「リセット」ボタンを押して、以下のように数値が初期化されれば OK です。

vue_8-4-3.png

2-2. $patch() メソッド

① 基本構文($patch オブジェクトによる更新)

$reset() メソッドの基本構文は次のとおりです。
更新する state のプロパティと値のセットを指定します。
全てのプロパティを指定する必要はなく、更新したいプロパティのみ指定します。

store.$patch({
  count: store.count + 1,
  age: 120,
  name: 'DIO',
})

② 基本構文($patch 関数による更新)

なお、次のように、アロー関数の形式で記述することもできます。
引数で受け取る state を使用することで、配列型のプロパティへの要素の追加(push)などの処理も可能となります。

store.$patch((state) => {
  state.items.push({ name: 'shoes', quantity: 1 })
  state.hasChanged = true
})

③ コードの修正

こちらも、実際にコードを記載して動作確認をしてみましょう。
src\components\Home.vue コンポーネントに赤枠部分を追加します。

vue_8-4-4.png

全てのコードをテキストで表示
src\components\Home.vue
<script setup>
import { useCounterStore } from '../stores/counter'
const store = useCounterStore()
const onPatch = () => {
  store.$patch({
    count: store.count - 1,
    name: 'DIO',
  })
}
</script>

<template>
  <div>Home</div>
  <button @click="store.increment">加算</button>
  <button @click="store.$reset()">リセット</button>
  <button @click="onPatch">更新</button>
  <div>name: {{ store.name }}</div>
  <div>count: {{ store.count }}</div>
  <div>doubleCount: {{ store.doubleCount }}</div>
</template>

追加したのは、次の部分です。

script

const onPatch = () => {
  store.$patch({
    count: store.count - 1,
    name: 'DIO',
  })
}

template

<button @click="onPatch">更新</button>

上記の「更新」ボタンを押すと、onPatch メソッドが呼び出され、script 側で定義した $patch が実行されます。

④ 動作確認

ブラウザを開いて確認をしてみましょう。
「更新」ボタンを押すと表示内容が更新され、押すたびに count の値が 1 ずつ減算されていくことが確認できるはずです。

vue_8-4-5.png

2-3. $state() メソッド

① 基本構文

$state() メソッドの基本構文は次のとおりです。指定した値で state を置き換えます。
全てのプロパティを指定する必要はなく、置換えを行うプロパティのみ指定します。

store.$state = { count: 24 }

$state() メソッドの実行時には、内部的に $patch() メソッドが呼び出されるため、実際は次の処理を行っていることと同義になります。

store.$patch((state) => { state.count: 24 })

② コードの修正

$state() についても、実際にコードを記載して動作確認をしてみましょう。
src\components\Home.vue コンポーネントに赤枠部分を追加します。

vue_8-4-6.png

全てのコードをテキストで表示
src\components\Home.vue
<script setup>
import { useCounterStore } from '../stores/counter'
const store = useCounterStore()
const onPatch = () => {
  store.$patch({
    count: store.count - 1,
    name: 'DIO',
  })
}
const onState = () => {
  store.$state = { count: 5 }
}
</script>

<template>
  <div>Home</div>
  <button @click="store.increment">加算</button>
  <button @click="store.$reset()">リセット</button>
  <button @click="onPatch">更新</button>
  <button @click="onState">置換え</button>
  <div>name: {{ store.name }}</div>
  <div>count: {{ store.count }}</div>
  <div>doubleCount: {{ store.doubleCount }}</div>
</template>

追加したのは、次の部分です。

script

const onState = () => {
  store.$state = { count: 5 }
}

template

<button @click="onState">置換え</button>

上記の「置換え」ボタンを押すと、onState メソッドが呼び出され、script 側で定義した $state が実行されます。

③ 動作確認

ブラウザを開いて確認をしてみましょう。
「置換え」ボタンを押すたびに count の値が 5 に変わります。

vue_8-4-7.png

なお「更新」ボタンを押して「置換え」ボタンを押すと、name は置換えの対象ではないことから「DIO」のまま変わらないことも確認してみてください。

vue_8-4-8.png

3. State の監視

$subscribe() メソッドを使用することにより、State の値の変化を監視することができます。

3-1. 基本構文

$subscribe() メソッドの基本構文は次のとおりです。

store.$subscribe((mutation, state) => {
  // 変異のタイプ('direct' | 'patch object' | 'patch function')を取得
  mutation.type
  // 対象 Store の Id を取得
  mutation.storeId
  // $patch() メソッドにオブジェクトを渡した場合のみ変更内容を取得
  mutation.payload

  // 変更後の state の内容を取得(必要な処理を行う)
  console.log(state)
})

$subscribe() メソッドに指定するコールバック関数では、第 1 引数の mutation から変更のタイプなどを取得することができ、第 2 引数の state からは、変更後の state の値が取得できます。

① mutation のプロパティ

第 1 引数の mutationから取得できるプロパティは次のとおりです。

No プロパティ 説明
1 type MutationType(変異のタイプ)を取得(※種類は次の表を参照)
2 storeId 対象 Store の Id を取得(defineStore() メソッドの第 1 引数に指定したストア名です)
3 payload $patch() メソッドにオブジェクトを渡した場合(type が 'patch object' の場合)のみ変更内容を取得

② MutationType(変異のタイプ)

mutation.type から取得されるタイプは、次の 3 種類です。

MutationType 説明
direct store.name = 'new name' state を直接変更
patch function store.$patch(state => state.name = 'newName') $patch 関数で state を変更
patch object store.$patch({ name: 'newName' }) $patch オブジェクトで state を変更

$patch$state を含む)以外の方法で state を更新した場合の MutationType は、基本的に「direct」となります。

実際に確認した方が早いので、以下、コードを記述して確認していきます。

3-2. コードの修正

src\components\Home.vue コンポーネントに赤枠部分を追加します。

vue_8-4-9.png

全てのコードをテキストで表示
src\components\Home.vue
<script setup>
import { useCounterStore } from '../stores/counter'
const store = useCounterStore()
const onPatch = () => {
  store.$patch({
    count: store.count - 1,
    name: 'DIO',
  })
}
const onState = () => {
  store.$state = { count: 5 }
}
store.$subscribe((mutation, state) => {
  console.log(mutation.type)
  console.log(mutation.storeId)
  console.log(mutation.payload)
  console.log(state)
})
</script>

<template>
  <div>Home</div>
  <button @click="store.increment">加算</button>
  <button @click="store.$reset()">リセット</button>
  <button @click="onPatch">更新</button>
  <button @click="onState">置換え</button>
  <div>name: {{ store.name }}</div>
  <div>count: {{ store.count }}</div>
  <div>doubleCount: {{ store.doubleCount }}</div>
</template>

追加したのは、次の部分です。

store.$subscribe((mutation, state) => {
  console.log(mutation.type)
  console.log(mutation.storeId)
  console.log(mutation.payload)
  console.log(state)
})

state の変更をキャッチしたときに、mutationstate の内容を Console 画面に表示していきます。

3-3. 動作確認

それでは、ブラウザで開いて確認をしましょう。
次のように、開発者画面から Console 画面を開いておきます。

vue_8-4-10.png

① actions による変更

まず「加算」ボタンを押します。加算ボタンは actions から値を更新します。
mutation.type は「direct」、mutation.storeId は「counter」と表示されています。
$patch オブジェクトは使用していないので、mutation.payload は「undefined」です。
なお、当然ですが、最後の state は、画面の表示内容と一致しています。

vue_8-4-11.png

② $reset() による変更

次に $reset() メソッドを実行する「リセット」ボタンを押します。
mutation.type は「patch function」となっており、$reset() メソッドは、$patch 関数を使用していることが分かります。

vue_8-4-12.png

③ $patch() による変更

続いて $patch() メソッドを実行する「更新」ボタンを 2 回押します。
$patch() オブジェクトを使用しているため、mutation.type は「patch object」となっています。 mutation.payload には、$patch() オブジェクトとして指定された内容が、それぞれ取得されています。

vue_8-4-13.png

④ $state() による変更

最後に $state() メソッドを実行する「置換え」ボタンを押します。
mutation.type は「patch function」となっていることから、内部的に $patch 関数が実行されていることが分かります。

vue_8-4-14.png

以上、$subscribe() メソッドについて、一通りの動作確認を行いました。
実務においては、引数の mutation および state から得られる値を使用しつつ、必要な処理を実装していくことになります。

Lesson 8 Chapter 5
Getters(ゲッター)

1. 基本的な使用方法

1-1. Getters の定義例

基本的に、Getters(ゲッター)は、State の値を用いた計算値を返却します。

① Option Stores 構文

Option Stores 構文では、以下のように、第 1 引数に state を受け取るアロー関数式で定義をします。

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
  }),
  getters: {
    doubleCount: (state) => state.count * 2,
  },
})

② Setup Stores 構文

Setup Stores 構文で Getters を定義する場合は、computed を使用します。
つまり、Getters は、State の値を用いた Computed(算出プロパティ)と同義ということになります。

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const doubleCount = computed(() => count.value * 2)

  return { count, doubleCount }
})

1-2. コンポーネントから Getters へのアクセス

次のように、Store のインスタンスを介して、コンポーネントから Getters にアクセスすることができます。

<script setup>
import { useCounterStore } from './stores/counter'
const store = useCounterStore()
</script>

<template>
  <p>Double count is {{ store.doubleCount }}</p>
</template>

2. Store 内における他の Getters へのアクセス

2-1. 定義例

下記コード中の doubleCountPlusOne() のように、this を使用して、他の Getters(ここでは doubleCount)を組み合わせて計算値を返却することができます。

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
  }),
  getters: {
    doubleCount: (state) => state.count * 2,
    doubleCountPlusOne() {
      return this.doubleCount + 1
    },
  },
})

なお、this を使用する場合は、アロー関数式は使用できないため、一般的な関数の形式で記述する必要があります。 さらに、型推論が効かなくなりますが、このカリキュラムでは特に気にしなくとも大丈夫です(※)。

型の定義を行う場合

型を定義するには、次のような方法があります。

① TypeScript で型定義を行う
TypeScript を使用すると厳密な型定義をすることができます(TypeScript は本カリキュラムでは使用しません)。

doubleCountPlusOne(): number {
  return this.doubleCount + 1
},

② JSDoc を使用して型のヒントを与える
TypeScript を使用しない場合は、JSDoc という JavaScript 用のコメント記法を用いることで、型のヒントを追加することも可能です。

/**
 * @returns {number}
 */
doubleCountPlusOne() {
  return this.doubleCount + 1
},

VSCode で定義すると、次のようになります。

vue_8-5-1.png

Getters 関数にカーソルを合わせると型のヒントが表示されます。

vue_8-5-2.png

2-2. 具体例で確認

それでは、 Store 内の他の Getters を使用する場合について、実際にコードを書いて確認してみましょう。

① counter ストアの修正

src\stores\counter.js ファイルに、次の赤枠部分を追加します。

vue_8-5-3.png

全てのコードをテキストで表示
src\stores\counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => {
    return {
      count: 0,
      name: 'Eduardo'
    }
  },
  getters: {
    doubleCount: (state) => state.count * 2,
    doubleCountPlusOne() {
      return this.doubleCount + 1
    },
  },
  actions: {
    increment() {
      this.count++
    },
  },
})

② Home.vue コンポーネントの修正

src\components\Home.vue ファイルの template に、次の赤枠部分を追加します。

vue_8-5-4.png

全てのコードをテキストで表示
src\components\Home.vue
<script setup>
import { useCounterStore } from '../stores/counter'
const store = useCounterStore()
const onPatch = () => {
  store.$patch({
    count: store.count - 1,
    name: 'DIO',
  })
}
const onState = () => {
  store.$state = { count: 5 }
}
store.$subscribe((mutation, state) => {
  console.log(mutation.type)
  console.log(mutation.storeId)
  console.log(mutation.payload)
  console.log(state)
})
</script>

<template>
  <div>Home</div>
  <button @click="store.increment">加算</button>
  <button @click="store.$reset()">リセット</button>
  <button @click="onPatch">更新</button>
  <button @click="onState">置換え</button>
  <div>name: {{ store.name }}</div>
  <div>count: {{ store.count }}</div>
  <div>doubleCount: {{ store.doubleCount }}</div>
  <div>doubleCountPlusOne: {{ store.doubleCountPlusOne }}</div>
</template>

③ ブラウザで動作確認

ブラウザで開いて「加算」ボタンを何回か押してみましょう。
新しく追加した「doubleCountPlusOne」の数値が、doubleCount に 1 加算した値で変動することが確認できると思います。

vue_8-5-5.png

3. 他の Store の Getters へのアクセス

3-1. 定義例

次のように他の Store をインポートすることで、その Store の Getters にアクセスすることもできます(公式サイト参照)。

import { useOtherStore } from './other-store'

export const useStore = defineStore('main', {
  state: () => ({
    // ...
  }),
  getters: {
    otherGetter(state) {
      const otherStore = useOtherStore()
      return state.localData + otherStore.data
    },
  },
})

上記のように Getters の関数内で、他の Store のインスタンス(ここでは otherStore)を取得してそのまま使用することになります。

3-2. 具体例で確認

それでは、他の Store の Getters を使用する場合について、実際にコードを書いて確認してみましょう。

① user ストアの修正

src\stores\user.js ファイルに Getters を 1 つ追加します(赤枠部分)。
id に 10000 を加算したものを globalId として取得するだけのものです。

vue_8-5-6.png

全てのコードをテキストで表示
src\stores\user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => {
    return {
      id: 1
    }
  },
  getters: {
    globalId: (state) => state.id + 10000
  }
})

② counter ストアの修正

src\stores\counter.js ファイルに、次の赤枠部分を追加します。

vue_8-5-7.png

全てのコードをテキストで表示
src\stores\counter.js
import { defineStore } from 'pinia'
import { useUserStore } from './user'

export const useCounterStore = defineStore('counter', {
  state: () => {
    return {
      count: 0,
      name: 'Eduardo'
    }
  },
  getters: {
    doubleCount: (state) => state.count * 2,
    doubleCountPlusOne() {
      return this.doubleCount + 1
    },
    idAndCount: (state) => {
      const userStore = useUserStore()
      return `${userStore.globalId}_${state.count}`
    },
  },
  actions: {
    increment() {
      this.count++
    },
  },
})

user ストアをインポートして、それを idAndCount という Getters で読み込んで使用しています。
返却値は、user ストアから取得した globalId と、counter ストアの count を文字列として結合したものを返します。

③ App.vue コンポーネントの修正

src\App.vue ファイルに次の赤枠部分を追加します。
v-model に number 修飾子を使用すると、テキストボックスに入力した値を数値型としてバインドすることができます(これを入れないと入力値が文字列として認識され、四則演算をすることができません)。

vue_8-5-8.png

全てのコードをテキストで表示
src\App.vue
<script setup>
import { useUserStore } from './stores/user'
const userStore = useUserStore()
</script>

<template>
  <div>ユーザーID: <input v-model.number="userStore.id"/></div>
  <p>
    <router-link to="/">Home</router-link> | 
    <router-link to="/about">About</router-link> | 
    <router-link :to="`/user/${userStore.id}`">UserPage</router-link>
  </p>
  <router-view></router-view>
</template>

<style scoped>
</style>

④ Home.vue コンポーネントの修正

最後に、src\components\Home.vue コンポーネントの template に、次の赤枠部分を追加します。

vue_8-5-9.png

全てのコードをテキストで表示
src\components\Home.vue
<script setup>
import { useCounterStore } from '../stores/counter'
const store = useCounterStore()
const onPatch = () => {
  store.$patch({
    count: store.count - 1,
    name: 'DIO',
  })
}
const onState = () => {
  store.$state = { count: 5 }
}
store.$subscribe((mutation, state) => {
  console.log(mutation.type)
  console.log(mutation.storeId)
  console.log(mutation.payload)
  console.log(state)
})
</script>

<template>
  <div>Home</div>
  <button @click="store.increment">加算</button>
  <button @click="store.$reset()">リセット</button>
  <button @click="onPatch">更新</button>
  <button @click="onState">置換え</button>
  <div>name: {{ store.name }}</div>
  <div>count: {{ store.count }}</div>
  <div>doubleCount: {{ store.doubleCount }}</div>
  <div>doubleCountPlusOne: {{ store.doubleCountPlusOne }}</div>
  <div>idAndCount: {{ store.idAndCount }}</div>
</template>

⑤ ブラウザで動作確認

それでは、ブラウザで表示してみましょう。
「加算」ボタンを何回か押すと、それに連動して「idAndCount」の末尾の数値がリアクティブに変動することが確認できます。

vue_8-5-10.png

次に「ユーザーID」欄に 123 という数字を入力します
すると、入力に連動して「idAndCount」の前半部分の数値がリアクティブに変動します。
この前半部分の数値は、他の Store である user ストアの Getters から取得したものです。

vue_8-5-11.png

以上のとおり、複数の Store から取得した値でも、リアクティブに反映されることが確認できました。

4. Getters に引数を渡す

4-1. 定義例

基本的に Getters に引数を渡すことはできません。ただし、Getters の戻り値を関数とすることで引数を渡すことが可能になります(公式サイト参照)。

export const useStore = defineStore('main', {
  getters: {
    getUserById: (state) => {
      return (userId) => state.users.find((user) => user.id === userId)
    },
  },
})

コンポーネント側では次のように使用します。

<script setup>
const store = useStore()
</script>

<template>
  <p>User 2: {{ store.getUserById(2) }}</p>
</template>

4-2. 具体例で確認

それでは、実際にコードを書いて確認してみましょう。

① counter ストアの修正

src\stores\counter.js ファイルに、次の赤枠部分を追加します。

vue_8-5-12.png

全てのコードをテキストで表示
src\stores\counter.js
import { defineStore } from 'pinia'
import { useUserStore } from './user'

export const useCounterStore = defineStore('counter', {
  state: () => {
    return {
      count: 0,
      name: 'Eduardo'
    }
  },
  getters: {
    doubleCount: (state) => state.count * 2,
    doubleCountPlusOne() {
      return this.doubleCount + 1
    },
    idAndCount: (state) => {
      const userStore = useUserStore()
      return `${userStore.globalId}_${state.count}`
    },
    multiplyCount: (state) => {
      return (num) => state.count * num
    },
  },
  actions: {
    increment() {
      this.count++
    },
  },
})

multiplyCount という Getters を追加しています。
引数に数値 num をとり、返却値として countnum を乗算した値を返します。

② Home.vue コンポーネントの修正

src\components\Home.vue コンポーネントの template に、次の赤枠部分を追加します。

vue_8-5-13.png

全てのコードをテキストで表示
src\components\Home.vue
<script setup>
import { useCounterStore } from '../stores/counter'
const store = useCounterStore()
const onPatch = () => {
  store.$patch({
    count: store.count - 1,
    name: 'DIO',
  })
}
const onState = () => {
  store.$state = { count: 5 }
}
store.$subscribe((mutation, state) => {
  console.log(mutation.type)
  console.log(mutation.storeId)
  console.log(mutation.payload)
  console.log(state)
})
</script>

<template>
  <div>Home</div>
  <button @click="store.increment">加算</button>
  <button @click="store.$reset()">リセット</button>
  <button @click="onPatch">更新</button>
  <button @click="onState">置換え</button>
  <div>name: {{ store.name }}</div>
  <div>count: {{ store.count }}</div>
  <div>doubleCount: {{ store.doubleCount }}</div>
  <div>doubleCountPlusOne: {{ store.doubleCountPlusOne }}</div>
  <div>idAndCount: {{ store.idAndCount }}</div>
  <div>multiplyCount: {{ store.multiplyCount(5) }}</div>
</template>

ゲッター multiplyCount に、引数として 5 を渡しています。
これにより、count5 を乗算した値が常に返却されることになります。

③ ブラウザで動作確認

それでは、ブラウザで表示してみましょう。
「加算」ボタンを押すたびに「multiplyCount」に、count5 を乗算した数値が表示されることが確認できれば OK です。

vue_8-5-14.png

以上、Getters に引数を渡す方法について確認することができました。

Lesson 8 Chapter 6
Actions(アクション)

1. 基本的な使用方法

1-1. Actions の定義例

Actions(アクション)には、State の値を変更する等のロジックを定義します。
Getters とは異なり、自由に引数を設定することができ、非同期処理(※)を行うこともできます。
そのため、サーバから API を呼び出したり、他のアクションを実行するなど、自由度の高い処理を記述することができるようになっています。

非同期処理

非同期処理とは、あるタスクの実行中に、その終了を待たずに別のタスクを実行する処理のことを言います。

Web アプリケーションでは「非同期通信」という言葉がよく出てきます。
これは、サーバーとの通信の際に、その応答を待たずに別の処理を進めることができる通信方法のことを指します。

① Option Stores 構文

まず、Option Stores 構文で Actions を定義する例です。
公式ページの記載例は、次のようになっています。

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
  }),
  actions: {
    increment() {
      this.count++
    },
    randomizeCounter() {
      this.count = Math.round(100 * Math.random())
    },
  },
})

基本的には、一般的なメソッド定義と変わりませんが、this を使用するためアロー関数式での記述はできません。

② Setup Stores 構文

Setup Stores 構文で Actions を定義する場合は、メソッドとして記述します。
つまり、Actions は、コンポーネントにおけるメソッドと同義ということになります。

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const increment = () => count.value++
  const randomizeCounter = () => count.value = Math.round(100 * Math.random())

  return { count, doubleCount }
})

1-2. コンポーネントから Actions へのアクセス

次のように、Store のインスタンスを介して、コンポーネントから Actions にアクセスすることができます。これも、今まで見てきたとおりです。

<script setup>
const store = useCounterStore()
store.randomizeCounter()
</script>

1-3. 他の Store の Actions へのアクセス

次のように他の Store をインポートすることで、その Store のアクション(Actions)にアクセスすることもできます(公式サイトより)。

import { useAuthStore } from './auth-store'

export const useSettingsStore = defineStore('settings', {
  state: () => ({
    preferences: null,
    // ...
  }),
  actions: {
    async fetchUserPreferences() {
      const auth = useAuthStore()
      if (auth.isAuthenticated) {
        this.preferences = await fetchPreferences()
      } else {
        throw new Error('User must be authenticated')
      }
    },
  },
})

上記の例では、アクション内で、const auth = useAuthStore() というように他の Store を呼び出し、そのアクション(ここでは isAuthenticated)を使用する形になっています。
この使用方法は、他の Store のゲッター(Getters)を使用する場合と基本的に同じのため、実例での確認は省略します。

2. Actions で非同期通信を行う

先に述べましたように、Actions で非同期通信を使用することができます。
ここでは、実際に API を使用した非同期通信を実装してみましょう。

2-1. 使用する API

気象庁が提供している天気予報の API を使用します。
https://www.jma.go.jp/bosai/forecast/data/overview_forecast/130000.json

このリンクをクリックすると、東京の天気の概況が JSON 形式で取得できます。

vue_8-6-1.png

取得された JSON オブジェクトの内容を開発者用画面の「Network」タブの「Preview」から確認しておきます。

vue_8-6-1-2.png

上の画像より、この API で取得できるプロパティは次の 5 つということが確認できます。
・headlineText
・publishingOffice
・reportDatetime
・targetArea
・text

2-2. weather ストアの追加

src\stores ディレクトリに、weather.js ファイルを追加してください。

vue_8-6-2.png

weather.js ファイルに記述したコードは次のとおりです。

src\stores\weather.js
import { defineStore } from 'pinia'

export const useWeatherStore = defineStore('weather', {
  state: () => {
    return {
      overview: {
        headlineText: '',
        publishingOffice: '',
        reportDatetime: '',
        targetArea: '',
        text: '',
      }
    }
  },
  actions: {
    async fetchForecast() {
      const url = 'https://www.jma.go.jp/bosai/forecast/data/overview_forecast/130000.json'
      const response = await fetch(url);
      this.overview = await response.json();
    }
  }
})

以下、記載している内容について見ていきます。

① state の定義内容

state には、API から取得したデータを格納するための overview オブジェクトを定義しています。

overview: {
  headlineText: '',
  publishingOffice: '',
  reportDatetime: '',
  targetArea: '',
  text: '',
}

この overview オブジェクトの各プロパティは、先ほど、実際の API から確認したプロパティをそのまま定義しています。

② actions の定義内容

actions には、fetchForecast() という関数を定義しています。

async fetchForecast() {
  const url = 'https://www.jma.go.jp/bosai/forecast/data/overview_forecast/130000.json'
  const response = await fetch(url);
  this.overview = await response.json();
}

・fetch() 関数
サーバとの通信には、fetch() 関数を使用しています。この関数は非同期で使用する必要があるため、async、await を使用して API の呼出しを実装しています。
今回のように、特定の URL からデータを取得する場合は、fetch('指定URL') の振り合いで、引数に URL を指定するだけで値の取得が可能となっています。

・json() 関数
json() 関数を使用することで、JSON 形式のデータを JavaScript のオブジェクト形式に変換することができます。

2-3. About.vue コンポーネントの修正

About.vue コンポーネントに赤枠部分を追加して、緑枠部分を削除します。

vue_8-6-3.png

修正後の About.vue ファイルは次のようになります。

src\components\About.vue
<script setup>
import { useWeatherStore } from '../stores/weather'
const weatherStore = useWeatherStore()
</script>

<template>
  <div>About</div>
  <button @click="weatherStore.fetchForecast">天気取得</button>
  <table>
    <tr><th>提供元</th><td>{{ weatherStore.overview.publishingOffice }}</td></tr>
    <tr><th>報告日時</th><td>{{ weatherStore.overview.reportDatetime }}</td></tr>
    <tr><th>場所</th><td>{{ weatherStore.overview.targetArea }}</td></tr>
    <tr><th>概要</th><td>{{ weatherStore.overview.text }}</td></tr>
  </table>
</template>

<style scoped>
tr {
  vertical-align: top;
  text-align: left;
  white-space: pre-wrap;
}
th {
  width: 80px;
}
</style>

以下、記載している内容のうち Actions に関係のあるところを確認しておきます。

① weather ストアのインスタンス取得

次のところで、weather ストアのインスタンスを取得しています。

import { useWeatherStore } from '../stores/weather'
const weatherStore = useWeatherStore()

指定内容は、特に今までと変わりません。

② API 呼出しの実行

API の呼出しを行う fetchForecast アクションは、次のボタンで呼び出されます。

<button @click="weatherStore.fetchForecast">天気取得</button>

2-4. ブラウザで動作確認

それでは、ブラウザで表示して、動作確認をしてみましょう。

vue_8-6-4.png

「天気取得」ボタンを押してみましょう。
成功すれば、次のように API で取得したデータが画面に表示されるはずです。

vue_8-6-5.png

3. Action の監視

store.$onAction() メソッドを使用することにより、Action の監視をすることができます。

3-1. 基本構文

store.$onAction() メソッドの基本構文は次のとおりです。

store.$onAction(({ name, store, args, after, onError }) => {
  console.log(name) // アクション名
  console.log(store.$id) // store のインスタンス
  console.log(args) // アクションの引数
  after((result) => console.log(result)) // アクション成功後の処理(引数 result は戻り値)
  onError((error) => console.log(error)) // アクション失敗時の処理
})

対象の Store 内のアクションが実行されると、store.$onAction() メソッドも発火します。
引数から 5 つのプロパティ namestoreargsafteronError が取得できます。

No プロパティ 説明
1 name 実行されたアクション名を取得
2 store 対象 store のインスタンスを取得
3 args 実行されたアクションの引数を取得
3 after() アクション成功後の処理をコールバック関数で記述(引数 result は戻り値)
3 onError() アクション失敗時の処理をコールバック関数で記述

3-2. 具体例で確認

それでは、store.$onAction() について、実際にコードを書いて確認してみましょう。

① weather ストアの修正

動作確認のため、weather ストアのアクション fetchForecast に引数と戻り値を追加しておきます。
src\stores\weather.js ファイルの、次の赤枠部分を追加・修正してください。

vue_8-6-6.png

全てのコードをテキストで表示
src\stores\counter.js
import { defineStore } from 'pinia'

export const useWeatherStore = defineStore('weather', {
  state: () => {
    return {
      overview: {
        headlineText: '',
        publishingOffice: '',
        reportDatetime: '',
        targetArea: '',
        text: '',
      }
    }
  },
  actions: {
    async fetchForecast(code) {
      const url = `https://www.jma.go.jp/bosai/forecast/data/overview_forecast/${code}.json`
      const response = await fetch(url)
      this.overview = await response.json()
      return 'success!'
    }
  }
})

引数に code を追加しています。
この値が 130000 の場合は東京の天気概要が表示されることになります。

② About.vue コンポーネントの修正

src\components\About.vue ファイルにつき、次の赤枠部分を追加・修正します。

vue_8-6-7.png

全てのコードをテキストで表示
src\components\About.vue
<script setup>
import { ref } from 'vue'
import { useWeatherStore } from '../stores/weather'
const code = ref('130000')
const weatherStore = useWeatherStore()

const fetchForecast = async () => {
  try {
    await weatherStore.fetchForecast(code.value)
  } catch {
    console.log('アクションが実行できませんでした。')
  }
}

weatherStore.$onAction(({ name, store, args, after, onError }) => {
  console.log(name)
  console.log(store.$id)
  console.log(args)
  after((result) => console.log(result))
  onError((error) => console.log(error))
})
</script>

<template>
  <div>About</div>
  <button @click="fetchForecast">天気取得</button>
  <div>コード: <input v-model="code"/></div>
  <table>
    <tr><th>提供元</th><td>{{ weatherStore.overview.publishingOffice }}</td></tr>
    <tr><th>報告日時</th><td>{{ weatherStore.overview.reportDatetime }}</td></tr>
    <tr><th>場所</th><td>{{ weatherStore.overview.targetArea }}</td></tr>
    <tr><th>概要</th><td>{{ weatherStore.overview.text }}</td></tr>
  </table>
</template>

<style scoped>
tr {
  vertical-align: top;
  text-align: left;
  white-space: pre-wrap;
}
th {
  width: 80px;
}
</style>

見てのとおりですが、store.$onAction() メソッドを使用しているのは、次のところです。
これで weather ストアのアクションを監視することができます。

weatherStore.$onAction(({ name, store, args, after, onError }) => {
  console.log(name)
  console.log(store.$id)
  console.log(args)
  after((result) => console.log(result))
  onError((error) => console.log(error))
})

以下のところで、fetchForecast アクションを script 内のメソッドにして、実行時にエラーが出た場合の処理を加えました(これをせずとも実行はできますが、ブラウザの Console に無用なエラーを表示させないための処置です)。

const fetchForecast = async () => {
  try {
    await weatherStore.fetchForecast(code.value)
  } catch {
    console.log('アクションが実行できませんでした。')
  }
}

なお、引数の code は、次の input ボックスで指定できるようにしています。

<div>コード: <input v-model="code"/></div>

③ ブラウザで動作確認

ブラウザで開いて「About」リンクをクリックして About ページを開きます。
開発者用画面の Console も表示しておいてください。

vue_8-6-8.png

・成功時の実行結果
「コード」欄に「140000」と入力してから、「天気取得」ボタンをクリックします。
すると、次のように、神奈川県の天気概要が取得できます。

vue_8-6-9.png

上図の右側に、アクション名「fetchForecast」、ストアの ID 名「weather」、アクションの引数「140000」、成功時の処理として戻り値が取得できているのが確認できると思います。
引数は、配列形式で取得されます。

・失敗時の実行結果
次に、「コード」欄に「140001」と入力してから、「天気取得」ボタンをクリックします。
この場合は、該当するコードが存在しないため API 取得時にエラーが生じます。

vue_8-6-10.png

上図の右側に、アクション名「fetchForecast」、ストアの ID 名「weather」、アクションの引数「140001」が表示されていることが確認できます。
加えて、エラー時の処理として、エラーオブジェクトの内容(赤枠部分)が表示されていることも確認できます。

以上、本レッスンでは、状態管理の方法、とりわけ Vue の公式ライブラリである Pinia について学んできました。
状態管理は、現在のフロントエンド開発において必須の技術となります。Store の概念や、データの取得/更新の仕組みについて、よく押さえておくようにしてください。