yourmystar tech blog
著者: hecateball 公開日:

【Vue】provide/inject 完全理解ガイド

この記事はユアマイスター アドベントカレンダー 2022の 2 日目の記事です。

コンポーネント間の状態管理

provide()inject()は、親子関係にある Vue コンポーネント間で状態(データ)を共有するための方法のひとつです。 公式ドキュメントに基本的な概念の解説がありますので、一度目を通すことをおすすめします。

実際のアプリケーションにおいては、モーダルや入力フォームなどコンポーネントを分割したいという要求と状態を共有したいという要求が競合するケースで利用することになると思います。

props との違い

親子コンポーネント間の状態を共有する方法としては props がよく使われます。 props は手軽ですが、次のような問題があります。

  • 子コンポーネントのさらに子コンポーネントなど、より遠い子孫へのデータ共有がたいへん
  • props を指定することを親コンポーネントに強要する(≒ コンポーネント間結合が強くなり、再利用性を損なう)
  • 渡された props を子コンポーネント内で直接変更できない

store との違い

Vue では状態管理のために store ライブラリを利用することがあります。 代表的なものはVuexPiniaが挙げられます。

これらとの最も大きな違いは状態のライフサイクルをprovide()の起点となるコンポーネントと共有する(つまり、コンポーネントが破棄されれば状態も破棄される)ことだと思います。 「いつ、どうやって状態を初期化しよう?」という悩みから解放されることが大きな利点です。

ユアマイスターではいくつかの Vue アプリケーションを開発していますが、store ライブラリはまだ出番がありません。

基本的な使い方を覚えよう

状態(データ)を共有する起点となるコンポーネントの setup 内でprovide()を呼びます。 第 1 引数は状態を参照する際のキー、第 2 引数は状態そのものです。

<script setup lang="ts">
import { provide, ref } from 'vue'

const visible = ref(false)
provide('modal', visible)
</script>

<template>
  <div>
    <div>
      <button type="button" @click="visible = true">モーダルを開く</button>
    </div>
    <!-- ↓に子コンポーネントがあることにします -->
    <MyModal />
  </div>
</template>

子コンポーネント側では、inject() を使って状態を受け取ります。

<script setup lang="ts">
import { inject } from 'vue'

// 親コンポーネントがprovideした状態を受け取る
const visible = inject('modal')
</script>

<template>
  <div v-if="visible">
    <div>
      <button type="button" @click="visible = false">モーダルを閉じる</button>
    </div>
    <!-- ...... -->
  </div>
</template>

デフォルト値を設定しよう

キーに対応する状態がprovide()されていないコンポーネントでinject()を試みると、undefinedが返却されます。

状態がprovide()されているかどうかをいちいち気にしながらコンポーネントを記述するのは面倒ですし、 同じプロジェクトの他のエンジニアはどうせそんなことを気にせずに、あなたが書いたコンポーネントを好き勝手に利用しようするに違いありません。

気をつけないとundefinedになって予期せぬ挙動をするコンポーネントはストレスの源泉となります。 inject()の第 2 引数にはデフォルト値を指定できますので、これを利用してフェイルセーフなコンポーネントを作りましょう。

<script setup lang="ts">
import { inject, ref } from 'vue'

// provideされていない場合は(どうせ誰もvisible.valueをtrueにする能力がないので)false
const modal = inject('modal', ref(false))
</script>

ファクトリー関数を利用しよう

あまり知られていませんが、inject()はデフォルト値を生成するためにファクトリー関数を使うことができます。 デフォルト値を関数で生成する場合は第 2 引数に関数を渡し、第 3 引数にtrueを指定します(※第 3 引数を指定しない場合、第 2 引数に渡した関数そのものがデフォルト値として返却されてしまいます)。

デフォルト値を生成する際に関数を使うことができるということは、デフォルト値を生成する処理の一部としてprovide()できるということです。 こうしておけば、親コンポーネントでも子コンポーネントでもまったく同一の処理でinject()から状態の取得を行うことができます。

<script setup lang="ts">
import { inject, provide, ref } from 'vue'

// この処理は親コンポーネントでも子コンポーネントでも同様に振る舞う
const visible = inject(
  'modal',
  () => {
    const visible = ref(false)
    // ここでprovide
    provide('modal', visible)
    return visible
  },
  true,
)
</script>

composable にしよう

コード上にまったく同一の処理が出現した場合、ひとつにまとめたくなるのがエンジニアの性です。 近年の Vue では、composable と呼ばれる関数にコンポーネントのロジックを掃き出すべしとされています。

ここでもその導きに従い、前節で共通化することに成功したロジックを composable にしてみましょう。

import { ref, inject, provide, readonly } from 'vue'

export const useModal = () => {
  const visible = inject(
    'modal',
    () => {
      const visible = ref(false)
      provide('modal', visible)
      return visible
    },
    true,
  )

  // 値を操作する関数を提供し、意図しない操作を防ぐ
  const open = () => (visible.value = true)
  const close = () => (visible.value = false)

  // 値の操作方法を限定するため、readonlyにする
  return { visible: readonly(visible), open, close }
}

これを利用するコンポーネントは次のようになるでしょう。

<script setup lang="ts">
import { useModal } from '~/composalbes/modal'

const { visible, open } = useModal()
</script>

<template>
  <div>
    <div>
      <button type="button" @click="open">モーダルを開く</button>
    </div>
    <Modal />
  </div>
</template>

Vue コンポーネントが状態管理の詳細を意識しなくてよいという点が重要で、composables を利用する目的はここにあります。 状態(データ)やビジネスロジックをコンポーネントから隠蔽することで、Vue コンポーネントを見た目(デザイン)の観点だけで分割できるようになるなどのさまざまなメリットが生まれます。

発展編: composable をブラッシュアップしよう

業務で Vue アプリケーションを開発する方はここに書いてあることが役に立つかもしれません。

inject が返す値に型をつけたい

特段の工夫なくinject()を使うと返り値の型がunknownになり、TypeScript の民が激怒します。

怒れる TypeScript の民を鎮めるためには、provide()inject()のキーとしてInjectionKey<T>を使いましょう。 こうすることで、inject()の返す値の型が T に推論されるようになります。

import { InjectionKey, Ref } from 'vue'

// InjectionKeyの実体はSymbolです
const MODAL_KEY: InjectionKey<Ref<boolean>> = Symbol()

export const useModal = () => {
  const visible = inject(
    MODAL_KEY,
    () => {
      const visible = ref(false)
      provide(MODAL_KEY, visible)
      return visible
    },
    true,
  )

  const open = () => (visible.value = true)
  const close = () => (visible.value = false)

  return { visible: readonly(visible), open, close }
}

キーの衝突を避けたい

モーダルをたくさん使うような治安の悪いアプリケーションがたくさんあるとは思いませんが、ユースケースによってはキーを動的に変更したいという要望もあるかもしれません。 そのような場合は、composable の引数としてキーを受け取るようにするとよいでしょう。

import { InjectionKey, Ref } from 'vue'

const MODAL_KEY: InjectionKey<Ref<boolean>> = Symbol()

// キーの名前を考えることを強要するのは大変不親切なので、デフォルト引数を与える
export const useModal = (key: string | InjectionKey<Ref<boolean>> = MODAL_KEY) => {
  const visible = inject(
    key,
    () => {
      const visible = ref(false)
      provide(key, visible)
      return visible
    },
    true,
  )

  const open = () => (visible.value = true)
  const close = () => (visible.value = false)

  return { visible: readonly(visible), open, close }
}

アプリケーション全体に値を共有したい

実質的なグローバル変数になってしまうためあまりおすすめはしませんが、app.provide()を利用することでアプリケーション全体に値を共有することができます。

const app = createApp(/* ...... */)

app.provide('modal', ref(false))

ただし、グローバルな状態共有が必要な場合は Vuex 等それに特化したライブラリの利用を検討すべきだと思います。

ツイートするはてなブックマークに追加シェアする