🍍

Vue.js 3.2 でのデータ受け渡し/状態管理 基本パターン

2022/05/08に公開

コンポーネント間のデータ受け渡しやデータ管理(状態管理)について整理しました。大まかに以下のパターンに分けられると思います。グローバル管理はVuexではなくPiniaで示しました。

パターン 概要
props プロパティを用いて子コンポーネントにデータを受け渡し
emit 子コンポーネントでのイベント発生時に、データを親コンポーネントへ受け渡し
Provide/Inject コンポーネント階層の深さに関係なく、親コンポーネントは子以下のすべての階層にデータを受け渡し
Pinia Vuexと同様にグローバルでの状態管理が可能。すべてのコンポーネント間でデータ共有できる。

0.動作環境

Apple M1
macOS Monterey 12.3.1
Node.js 16.11.33
TypeScript 4.6.4
Vue.js 3.2.33
Vite 2.9.8
Pinia 2.0.14

1.インストール/コンポーネント設定

viteを使ってvue3をインストール。パッケージ名は今回dataflowとした。

npm init vue@3 dataflow

✔ Add TypeScript? … Yes
✔ Add JSX Support? … No
✔ Add Vue Router for Single Page Application development? … Yes
✔ Add Pinia for state management? … Yes
✔ Add Vitest for Unit Testing? … No
✔ Add Cypress for both Unit and End-to-End testing? … No
✔ Add ESLint for code quality? … No

プロジェクトディレクトリに移動し、パッケージインストールのうえ、開発サーバを起動。

cd dataflow
npm install
npm run dev

Vue.js初期画面が表示される。

Vue.js3.2の初期状態で既に良い感じでコンポーネントがツリー状になっているので、このまま流用して以下の通りデータ受け渡しを試みます。

今回、コードの見通しを良くするために、余計なCSSやImage等は予め削除します。

削除後のコード
App.vue
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import HelloWorld from '@/components/HelloWorld.vue'
</script>

<template>
  <header>

    <div class="wrapper">
      <HelloWorld msg="You did it!" />

      <nav>
        <RouterLink to="/">Home</RouterLink>
        <span> | </span>
        <RouterLink to="/about">About</RouterLink>
      </nav>
    </div>
  </header>

  <RouterView />
</template>
views/HomeView.vue
<script setup lang="ts">
import TheWelcome from '@/components/TheWelcome.vue'
</script>

<template>
  <main>
    <TheWelcome />
  </main>
</template>
views/AboutView.vue
<script setup lang="ts">
</script>

<template>
  <div class="about">
    <h1>This is an about page</h1>
  </div>
</template>
components/HelloWorld.vue
<script setup lang="ts">
defineProps<{
  msg: string
}>()
</script>

<template>
  <div class="greetings">
    <h1 class="green">{{ msg }}</h1>
    <h3>
      You’ve successfully created a project with
    </h3>
  </div>
</template>
components/TheWelcome.vue
<script setup lang="ts">
import WelcomeItem from './WelcomeItem.vue'
</script>

<template>
  <WelcomeItem>
    <template #heading>AAA</template>
    aaa
  </WelcomeItem>
</template>
components/WelcomeItem.vue
<script setup lang="ts">
</script>

<template>
  <div class="item">
    <div class="details">
      <h3>
        <slot name="heading"></slot>
      </h3>
      <slot></slot>
    </div>
  </div>
</template>

かなりシンプルな画面になりました。

2.Props

親コンポーネントで定義された子コンポーネントタグにプロパティおよび渡したい値を定義します。この場合、プロパティ名はparentToChildProp、渡したい値は親から子へです。

views/HomeView.vue
<template>
  <main>
    <TheWelcome parentToChildProp="親から子へ" />
  </main>
</template>

子コンポーネントのscriptタグ内に、先に設定したプロパティを呼び出すためのdefineProps()を定義します。
templateタグ内にてdefineProps名.親コンポーネントプロパティ名で値が呼び出されます。

components/TheWelcome.vue
<script setup lang="ts">
// ...略...
interface Props {
  parentToChildProp: string
}
const props = defineProps<Props>()
</script>

<template>
  <WelcomeItem>
    <template #heading>{{ props.parentToChildProp }}</template>
    aaa
  </WelcomeItem>
// ...略...
</template>

3.Emit

子コンポーネントのscriptタグ内に、refをインポートして渡したい値をref()に記述します。
defineEmits()を定義し、親コンポーネントに値を渡すためメソッドを定義し、その中にemit関数を記述します。
templateタグ内で値を渡すためのメソッドをイベントと結びつけます。

components/TheWelcome.vue
<script setup lang="ts">
// ...略...
import {ref} from 'vue'

const childValue = ref<string>("子から親へ")

interface Emits {
  (e: 'event-change', v: string): void
}
const emit = defineEmits<Emits>()

const onClick = (value: string) => {
  emit('event-change',value)
}
</script>

<template>
  <button type="button" @click="onClick(childValue)">ボタン</button>
// ...略...
</template>

親コンポーネントにrefをインポートし、ref()と呼び出し関数を定義します。
子コンポーネントタグTheWelcome内に、子コンポーネントで記述したemit関数の第一引数を@第一引数として記述し(この場合event-change)、先に定義したメソッド(この場合chileToParent)と結びつけます。こうして、子コンポーネントのイベントを検知することで値が受け渡されます。

views/HomeView.vue
<script setup lang="ts">
// ...略...
import {ref} from 'vue'
const receivedValue = ref<string>("")
const chileToParent = (e:string) => {
  receivedValue.value = e
}

</script>

<template>
  <main>
    <h3>{{receivedValue}}</h3>
    <TheWelcome parentToChildProp="親から子へ" @event-change="chileToParent"/>
  </main>
</template>

4.Provide/Inject

親コンポーネントにprovideをインポートします。受け渡したい値をref()に設定し、provide()を定義します。provide()の第一引数は値を渡すキーとなり、第二引数が実際に受け渡す値となります。

views/HomeView.vue
<script setup lang="ts">
// ...略...
import { ref, provide } from 'vue'

const provideValue = ref('親でprovideして、孫でinjectされます')
provide('key', provideValue)

</script>

子コンポーネント以下の階層すべてのコンポーネントでinjectは有効です(逆に言うと、provideされているツリー以外では無効)。injectしたいコンポーネントの、scriptタグ内でinjectをインポートし、先に設定した変数にinject(provideキー)とすることで値が受け渡されます。

components/WelcomeItem.vue
<script setup lang="ts">
import { inject } from 'vue'
const injectValue = inject('key')
</script>
<template>
// ...略...
  <div>{{ injectValue }}</div>
// ...略...
</template>

5.Pinia

デフォルトでstores/counter.tsが設定されてます。Piniaではここで状態が一元管理されます。今回はデフォルトコードをそのまま流用します。

stores/counter.ts
import { defineStore } from 'pinia'
export const useCounterStore = defineStore({
  id: 'counter',
  state: () => ({
    counter: 0
  }),
  getters: {
    doubleCount: (state) => state.counter * 2
  },
  actions: {
    increment() {
      this.counter++
    }
  }
})

Piniaストアを呼び出したいコンポーネントのscriptタグ内で、ストアuseCounterStoreをインポートし、useCounterStore()に名称を付けます。templateタグ内にてuseCounterStore()名.ストアのstate名useCounterStore()名.ストアのgetter名で値が呼び出され、useCounterStore()名.ストアのaction名にてストア内で定義したアクションが実行されます。

views/AboutView.vue
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter';
const storeCounter = useCounterStore()
</script>

<template>
// ...略...
    <h3>{{storeCounter.counter}}</h3>
    <h3>{{storeCounter.doubleCount}}</h3>
    <button @click="storeCounter.increment">+</button>
</template>

異なる階層のコンポーネントからもPiniaストアが同期的に参照されております。

components/TheWelcome.vue
<script setup lang="ts">
// ...略...
import { useCounterStore } from '@/stores/counter';
const storeCounter = useCounterStore()
</script>

<template>
// ...略...
  <h3>{{storeCounter.counter}}</h3>
  <h3>{{storeCounter.doubleCount}}</h3>
  <button @click="storeCounter.increment">+</button>
// ...略...
</template>

Pinia補足

scriptタグ内でPiniaストアのstateとgettersを取得するには、storeToRefs()もしくはcomputed()を用いる。

stores/counter.ts
<script setup lang="ts"> 
import { useCounterStore } from '@/stores/counter';
import { computed } from "@vue/reactivity";
import { storeToRefs } from 'pinia'

const storeCounter = useCounterStore()

// This won't be reactive.
// const { counter } = storeCounter

// This will be reactive.
const { counter } = storeToRefs(storeCounter)
const doublecount = computed(() => { return storeCounter.doubleCount })
</script> 

Discussion