Vue.js 3.2 でのデータ受け渡し/状態管理 基本パターン
コンポーネント間のデータ受け渡しやデータ管理(状態管理)について整理しました。大まかに以下のパターンに分けられると思います。グローバル管理は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等は予め削除します。
削除後のコード
<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>
<script setup lang="ts">
import TheWelcome from '@/components/TheWelcome.vue'
</script>
<template>
<main>
<TheWelcome />
</main>
</template>
<script setup lang="ts">
</script>
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<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>
<script setup lang="ts">
import WelcomeItem from './WelcomeItem.vue'
</script>
<template>
<WelcomeItem>
<template #heading>AAA</template>
aaa
</WelcomeItem>
</template>
<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
、渡したい値は親から子へ
です。
<template>
<main>
<TheWelcome parentToChildProp="親から子へ" />
</main>
</template>
子コンポーネントのscript
タグ内に、先に設定したプロパティを呼び出すためのdefineProps()
を定義します。
template
タグ内にてdefineProps名.親コンポーネントプロパティ名
で値が呼び出されます。
<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
タグ内で値を渡すためのメソッドをイベントと結びつけます。
<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
)と結びつけます。こうして、子コンポーネントのイベントを検知することで値が受け渡されます。
<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()
の第一引数は値を渡すキーとなり、第二引数が実際に受け渡す値となります。
<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キー)
とすることで値が受け渡されます。
<script setup lang="ts">
import { inject } from 'vue'
const injectValue = inject('key')
</script>
<template>
// ...略...
<div>{{ injectValue }}</div>
// ...略...
</template>
5.Pinia
デフォルトでstores/counter.ts
が設定されてます。Piniaではここで状態が一元管理されます。今回はデフォルトコードをそのまま流用します。
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名
にてストア内で定義したアクションが実行されます。
<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 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()
を用いる。
<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