Vue.jsのコンポーネント間で値の共有をする5つの方法|書き途中
やりたいこと
Vue.jsでコンポーネント間での値の共有をしたい。
親子関係問わず行えるとOK。
- やりたいこと
- 方法
- V-bind/Props/Emit
- State Management Library (etc: Vuex, Redux...
- EventHub/EventBus
- WIP: BrowserStorage (etc: localStorage, IndexedDB...
- Server (etc: firebase...
方法
主に以下の5つがありそう。
- V-bind/Props/Emit
- State Management Library (etc: Vuex, Redux...
- EventHub/EventBus
- WIP: BrowserStorage (etc: localStorage, IndexedDB...
- WIP: Server (etc: firebase...
V-bind/Props/Emit
親子関係のコンポーネントだとすごい楽に値を渡せる。
parent.vueのhogeをchild.vueにv-bindで渡し、
child.vueでは、propsで受け取り、
child.vueのemitでparent.vueに値と渡し、
parent.vueのメソッドイベントハンドラでhogeを更新。
ディレクトリ
- Project
- parent.vue
- child.vue
parent.vue
<template> <div> <child :hogehoge="hoge" @hogehogeHandler='fromChild' > </child> </div> </template> <script> import child from './child.vue' export default { data () { return { hoge: 'fuga' } }, components: { child }, methods: { fromChild (msg) { this.hoge = msg } } } </script>
child.vue
<template> <div> <p>{{ hogehoge }}</p> <button @click="hogehogeHandler">hogehoge -> fugafuga</button> </div> </template> <script> export default { props: ['hogehoge'], methods: { handler () { this.$emit('hogehogeHandler', 'fugafuga') } } } </script>
parent.vueの:hogehoge="hoge"
のイコールの左側はchild.vueのprops名になり、右側はparent.vueのdataのプロパティ名が入る。
parent.vueの@hogehogeHandler='fromChild'
のイコールの左側はchild.vueのemitの第一引数名になり、右側はparent.vueのmethods名または何かしらの処理が入る。
child.vueのthis.$emit('hogehogeHandler', 'fugafuga')
は、第一引数にはparent.vueの@hogehogeHandler='fromChild'
のイコールの左側が入り、第二引数以降には渡したい値が入る。
今回の場合はfugafuga
という文字列をparent.vueの@hogehogeHandler='fromChild'
を介しparent.vueのfromChild (msg)
の引数で受け取っている。
なお、$emitでたくさん値を渡したい場合は以下のようにobjectにして渡している。
this.$emit('foo', { bar: 'yahho', baz: 10000, qux: { quux: 'apikey' } })
State Management Library (etc: Vuex, Redux...
Vueでのコンンポーネント間で値を共有したいならとりあえずVuexを使うことがおすすめ。
筆者が他のライブラリについて詳しくないからなんとも言えないけど、VueでReduxを使うは、Vue.jsの良さみたいなものと相性が悪い気がしてならない。
なのでここでは、Vuexを紹介。
VuexにはStateの管理方法(値の共有の仕方?)には2つの方法が存在する。
- クラシックモード
- モジュールモード
今回は、モジュールモードのみ書く。(モジュールモードの方が好きだから)
ビルドは、parcelでします。
スプレッド演算子(...
)を利用できるようにbabelやtypescriptでトランスパイルできるように設定しないといけない。
だいぶ複雑なディレクトリになる。
foo.vueとbar.vueで設定した値をindex.vueで見る仕組みを以下に記入。
ディレクトリ
- src
- index.html
- index.js
- index.vue
- components
- foo.vue
- bar.vue
- store
- index.js
- modules
- baz.js
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Vuex</title> </head> <body> <div id="app"></div> <script src="./index.js"></script> </body> </html>
index.js
import Vue from 'vue' import Vuex from 'vuex' import App from './index.vue' import { store } from './store' Vue.use(Vuex) new Vue({ el: '#app', store, render: h => h(App) })
index.vue
<template> <div> {{ count }} <foo></foo> <bar></bar> </div> </template> <script> import { mapGetters } from 'vuex' import foo from './components/foo.vue' import bar from './components/bar.vue' export default { name: "App", components: { foo, bar }, computed: { ...mapGetters({ count: 'baz/count' }) } } </script>
components/foo.vue
<template> <div> <button @click="set({ count: 1000 })">set 1000</button> </div> </template> <script> import { mapActions } from 'vuex' export default { methods: { ...mapActions({ set: 'baz/set' }) } } </script>
components/bar.vue
<template> <div> <button @click="set({ count: 5 })">set 5</button> </div> </template> <script> import { mapActions } from 'vuex' export default { methods: { ...mapActions({ set: 'baz/set' }) } } </script>
store/index.js
import Vue from 'vue' import Vuex from 'vuex' import baz from './modules/baz' Vue.use(Vuex) export const store = new Vuex.Store({ modules: { baz } })
store/modules/baz.js
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const state = { count: 0 } const actions = { set ({ commit }, { count }) { commit("SET", { count }) } } const mutations = { SET (state, { count }) { state.count = count } } const getters = { count: state => state.count } export default { namespaced: true, state, getters, actions, mutations }
流れとしては、index.vueのcount(mapGettesのcount)でstore/modules/baz.jsのstateのcountを参照(購読?)しておく。
components/foo.vueのbutton押された場合(発行?)、
components/foo.vueのbuttonのset(mapActionsのset)で値(1000)を
store/modules/baz.jsのactionsのsetの第二引数で受け取り、
commitでstore/modules/baz.jsのmutationsのSETの第二引数で受け取り、
store/modules/baz.jsのstateに保存する。
保存するとindex.vueのcount(mapGettesのcount)は自動的に1000という値が入る。
index.vueのmapGetters
でstore/modules/baz.jsのgettersの準備。
index.vueのmapGettersのcount: 'baz/count'
ののkey(count)はdataのプロパティのように利用できる。
例えば、index.vueならthis.countのように利用できるようになっている。
value('baz/count')のスラッシュの左側baz
はstore/index.jsのmodules: { baz }で,
スラッシュの右側count
はstore/modules/baz.jsのgettersのcount。
ちなみにgetterに引数を持たせることが可能。
他に、mapGettersのstoreのmodule名を省略する書き方もある。
store/modules/baz.jsのcount: state => state.count
のkey(count)がindex.vueのcount: 'baz/count'
のvalue('baz/count')のスラッシュの右側にあたる。
count: state => state.count
のvalue(state => state.count)は、store/modules/baz.jsのconst state
のcountを読むようになっている。
Vuexのactionsを利用しなくても構わないが、非同期処理を行いたい場合はactionsのfunctionにasync/awaitを付けて非同期処理を行うことができる。
firebaseとかと連携したい場合は利用するかもしれない。
構造は違うけどサンプルリポジトリ。
EventHub/EventBus
ディレクトリ
src/ ├── Events.js └── components ├── Child.vue └── Parent.vue
components/Parent.vue
<template> <button @click="add">Parent</button> </template> <script> import { eventHub, EVENT_ADD } from '../Events'; export default { name: 'Parent', methods: { add() { eventHub.$emit(EVENT_ADD, "new item"); } } } </script>
components/Child.vue
<template> <ul> <li v-for="(item, index) in items" :key="index"> {{ item }} </li> </ul> </template> <script> import { eventHub, EVENT_ADD } from '../Events'; export default { name: 'Child', data() { return { items: [] } }, mounted() { eventHub.$on(EVENT_ADD, this.addItem); }, beforeDestroy() { eventHub.$off(EVENT_ADD, this.addItem); }, methods: { addItem(item) { this.items.push(item); } } } </script>
Events
import Vue from 'vue'; export const eventHub = new Vue(); export const EVENT_ADD = 'add';
流れ
Events.js
で Vue インスタンスを生成。ついでのイベント名を設定。- イベントを受信したいコンポーネント (ここでは
Child.vue
) に、イベント用のVueのインスタンスとイベント名をimport。 $on
メソッドでイベントを受信できるように設定。このときに、$on
メソッドの第1引数には、イベント名を、第2引数には、イベント時に発生する関数
を設定する。beforeDestroy()
時に、$off
メソッドで受信できるようにしたイベントを削除するようにしておく。- イベントを発生させたいコンポーネント (ここでは
Parent.vue
) に、イベント用のVueのインスタンスとイベント名をimport。 $emit
メソッドでイベントを発生できるように設定。$emit
メソッドの第1引数には、イベント名を、第2引数には、イベント時に発生する関数に与える値
を設定する。
簡単にまとめると、
1. $on
でイベントをlisten
2. $emit
でイベントを発生
3. $off
でイベントlistenを終了
参考
- Migration from Vue 1.x — Vue.js, 入手先 https://vuejs.org/v2/guide/migration.html#dispatch-and-broadcast-replaced
WIP: BrowserStorage (etc: localStorage, IndexedDB...
ディレクトリ
src/ ├── Storage.js └── components ├── Child.vue └── Parent.vue
components/Parent.vue
<template> <button @click="add">Parent</button> </template> <script> import { addItem } from '../Storage'; export default { name: 'Parent', methods: { add() { addItem('new item'); } } } </script>
components/Child.vue
<template> <ul> <li v-for="(item, index) in items" :key="index"> {{ item }} </li> </ul> </template> <script> import { getItems } from '../Storage'; export default { name: 'Child', data() { return { items: [] } }, mounted() { this.items = getItems(); } } </script>
Storage.js
const KEY_ADD = 'add'; export function getItems() { const raw = localStorage.getItem(KEY_ADD); return JSON.parse(raw) || []; } export function addItem(item) { const items = getItems(); items.push(item) return localStorage.setItem(KEY_ADD, JSON.stringify(items)); }
流れ
Server (etc: firebase...
wip