Hanatare's PaPa

Make life a little richer.

Virtual Space of Hanatare's PaPa

人生をほんの少しだけ充実させる

【Vue.js 3】複数階層を跨ぐコンポーネントのデータの受け渡し

前回の記事では、直接的な親子関係にあるコンポーネント間の通信について「props」と「emit」についてまとめました。

www.hanatare-papa.jp

しかし、アプリケーションが複雑になり、親子関係を超えた、コンポーネント同士でデータを共有したくなることがあります。例えば、一番上の階層にあるコンポーネントのデータを、5階層下の孫のようなコンポーネントで使いたい場合、中間の3つのコンポーネントは、自分では使わないデータをただ下へ下へと受け渡すためだけにpropsを定義しなければなりません。これは非常に冗長で、コンポーネントの再利用性を損ないます。

今回は、この「プロップスのバケツリレー」問題を解決し、より高度で柔軟な状態管理を実現するための2つの強力な手法、「provide/inject」と状態管理ライブラリ「Pinia」についてまとめたいと思います。

記事のポイント
  • プロップスバケツリレーを解消するprovideとinject
  • アプリケーション全体の状態を管理するPinia

provideとinject:プロップスバケツリレーからの解放

「provide」と「inject」は、Vueに組み込まれた「依存性の注入(Dependency Injection)」の仕組みです 。 これにより、中間のコンポーネントを介さずに、必要なコンポーネントへ直接データを届けることができます。

  • provide: 先祖コンポーネントがデータや機能を提供する(provide)。
  • inject: 子孫コンポーネントが、階層の深さに関わらず、そのデータや機能を注入して(inject)使用する。

基本的な使い方

「script setup」内でprovide関数とinject関数をインポートして使います。 provideの第一引数は「インジェクションキー」と呼ばれる一意の識別子で、通常は文字列かSymbolを使います 。

親コンポーネント (ParentComponent.vue)

<script setup>
import { provide } from 'vue'
import ChildComponent from './components/ChildComponent.vue'
// 'message'というキーで、'hello from ancestor!'という値を提供する
provide('message', 'hello from ancestor!')
</script>

<template>
  <p>現在の名前: {{ message }}</p>
  <ChildComponent />
</template>

子コンポーネント (ChildComponent.vue)

<script setup>
import DescendantComponent from './DescendantComponent.vue'
</script>
<template>
  <DescendantComponent />
</template>

孫コンポーネント (DescendantComponent.vue)

<script setup>
import { inject } from 'vue'

// 'message'というキーで提供された値を注入する
const message = inject('message')
</script>

<template>
  <p>先祖からのメッセージ: {{ message }}</p>
</template>

リアクティブなデータの提供

静的な値だけでなく、refやreactiveで作成したリアクティブなデータもprovideできます。これにより、提供元のデータが変更されると、注入先のデータも自動的に更新されます 。この時重要なポイントがあります。

それは、状態を変更するロジックも、状態を提供しているコンポーネント内にまとめることです 。もし注入した側で自由に状態を変更できてしまうと、データの流れが追跡しづらくなるという、propsの直接変更と同じ問題が再発してしまいます。状態(ref)と、その状態を更新する関数をセットでprovideするのが良い設計です。これにより、状態管理の責任の所在が明確になり、メンテナンス性が向上します。

親コンポーネント (ParentComponent.vue)

<script setup>
import { ref, provide } from 'vue'
import ChildComponent from './components/ChildComponent.vue'
const location = ref('Tokyo')
function updateLocation() {
  location.value = 'Osaka'
}

// 状態そのものと、
provide('location', location)
// 状態を更新する関数をセットで提供する
provide('updateLocation', updateLocation)
</script>

<template>
  <ChildComponent />
</template>

孫コンポーネント (DescendantComponent.vue)

<script setup>
import { inject } from 'vue'

const location = inject('location')
const updateLocation = inject('updateLocation')
</script>

<template>
  <p>現在の場所: {{ location }}</p>
  <button @click="updateLocation">場所を更新</button>
</template>

Pinia:アプリケーション全体のグローバルな状態管理

「provide/inject」は、特定のコンポーネントツリー内でデータを共有するには非常に有効です。しかし、ユーザーのログイン状態、テーマ設定、ショッピングカートの中身など、アプリケーションのどこからでもアクセスしたい「グローバルな状態」には、専用の状態管理ライブラリを使うのが一般的です 。

Vue 3における公式の状態管理ライブラリが「Pinia(ピニア)」です 。Vuexの後継と位置づけられており、よりシンプルで直感的なAPIと、強力なTypeScriptサポートが特徴です 。

Piniaの基本

Piniaでは、「ストア」という単位で状態を管理します。ストアは、「state」、「getters」、「actions」から構成されます。

  • state: ストアが保持するデータ本体。コンポーネントのdataに相当します。
  • getters: stateから派生した値を計算するプロパティ。コンポーネントのcomputedに相当します。
  • actions: stateを変更するためのメソッド。非同期処理も可能です。コンポーネントのmethodsに相当します。

ストアの定義(stores/counter.js)

import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'ハナタレ'
  }),
  getters: {
    doubleCount: (state) => state.count * 2,
  },
  actions: {
    increment() {
      this.count++;
    },
    async fetchAndSetName(newName) {
      // 非同期処理もアクション内で書ける
      await new Promise(resolve => setTimeout(resolve, 1000));
      this.name = newName;
    }
  },
});

コンポーネントでの利用

定義したストアは、どのコンポーネントからでもインポートして使用できます 。

<script setup>
import { useCounterStore } from '@/stores/counter';

// ストアのインスタンスを取得
const counterStore = useCounterStore();
</script>

<template>
  <div>
    <p>Count: {{ counterStore.count }}</p>
    
    <p>Double Count: {{ counterStore.doubleCount }}</p>
    
    <button @click="counterStore.increment">Increment</button>
  </div>
</template>

まとめ

前回と、今回で、Vue.jsのコンポーネント間通信についてまとめてきました。「props/emit」で、 親子間の明確な通信路を構築し、より複雑な階層になる場合は「provide/inject」を使ってより記述をシンプルに行い、グローバルな利用が必要であれば「Pinia」を使うというイメージになるかと思います。今回の記事が今回の記事がVue.jsを使う方の参考になれば幸いです。