JavaScript
Vue.js
vuenative

Vue Nativeで子コンポーネントから$emit()できない問題とその対策

この記事はU-Tokyo mech (東京大学機械系) Advent Calendar 2018の3日目の記事です。

機械系学部4年の@kn1chtと申します。趣味でWebやJavaScriptをやっており、今回もそのネタです。

昨日の記事は、本カレンダーの発起人である@hagi-sukeさんの「型で覚えるDeep Learningのライブラリ」でした。各種ライブラリのサンプルコードが揃っていて、実際に使おうという時に大変役立つのではないかと思います。


Vue Nativeは、有名なJavaScriptフレームワークであるVue.jsモバイルアプリ(iOS/Android)を作れるというものです。入門は凄まじく簡単で、説明に従ってコマンドをポチポチすれば(参考:さっそくVue NativeでHello Worldしてみた数分後には手元のスマホでアプリが動きます

もともとVue.jsを使っていたこともあり試しに簡単なアプリを作ってみたところ、コンポーネント間のデータ受け渡しでハマってしまったので、調べたことを書いていきます。


概要


環境

$ node -v

v10.10.0
$ npm ls -g | grep vue-native
├─┬ vue-native-cli@0.0.3
$ cat package.json | grep vue-native
"vue-native-core": "0.0.8",
"vue-native-helper": "0.0.9",
"vue-native-router": "0.0.1-alpha.3"
"vue-native-scripts": "0.0.14"


Vue Nativeとは

ネイティブアプリ向けのJavaScriptフレームワークには、FacebookのReact Nativeがあり、圧倒的な人気を誇ります。

Vue Nativeは、GeekyAntsというインドのWeb企業によるJavaScriptフレームワークです。その実態は、React NativeをVue.jsの書き方で使えるようにするラッパーです。

Vue.js最大の特徴の一つ、単一ファイルコンポーネントを使うことができ、Vue使いに嬉しいポイントとなっています。

また、React Nativeをラップしただけなので、React Nativeの豊富な知見やコード資産を流用できます。その反面、エラーメッセージがカオスだったり、React Native側のドキュメントを見る必要があったりと弱点もあります。

React Nativeとの関係については、同じくGeekyAnts製のコンポーネントライブラリであるNativeBaseのドキュメントを見ると雰囲気が分かりやすいでしょう。ReactとVueの書き方を切り替えて見ることができます。


現象

というわけで調子よくコードを書いていたのですが、コンポーネントを分け始めたところで詰まりました。


子から親にイベントを渡せない

Vue.jsには、コンポーネントの親子間でデータをやり取りするための機能が用意されています。

親から子に数値を渡し、子のボタンを押すとカウントアップして親に返すサンプルです(.sync修飾子を利用しています)。


src/parent.vue

<template>

<nb-root>
<nb-body :style="{ justifyContent : 'center' }">
<nb-h1>parent: {{ value }}</nb-h1>
<child :parentValue.sync="value" /> <!-- valueをparentValueとして子に渡している -->
</nb-body>
</nb-root>
</template>

<script>
import child from './child.vue';

export default {
data() {
return { value : 0 }
},
components : { child }
}
</script>



src/child.vue

<template>

<view>
<nb-h1>child: {{ value }}</nb-h1>
<nb-button :on-press="() => $emit('update:value', ++value)">
<nb-text>increment</nb-text>
</nb-button>
</view>
</template>

<script>
export default {
data() {
return { value : 0 }
},
props : { parentValue : Number }
}
</script>


これで、ボタンを押すと親子両方の値が増えるはずです。

sample-fail.gif

…増えていませんね。


省略せずに書いてみる

.syncは、


  • 親から子にv-bindでデータをバインドする

  • 子から親に$emit()したイベントをv-onで受け取る

というパターンを短く書ける構文です。

これを略さずに書いた上で、イベントが発生したがどうかも表示させてみます。



差分


src/parent.vue

@@ -2,7 +2,10 @@

<nb-root>
<nb-body :style="{ justifyContent : 'center' }">
<nb-h1>parent: {{ value }}</nb-h1>
- <child :parentValue.sync="value" />
+ <nb-h3>{{ message }}</nb-h3>
+ <child :parentValue="value"
+ @update="val => { value = val; message = 'update event emitted!' }"
+ />
</nb-body>
</nb-root>
@@ -12,7 +15,7 @@ import child from './child.vue';

export default {
data() {
- return { value: 0 }
+ return { message : 'event has not been emitted yet', value: 0 }
},
components: { child }
}


src/child.vue

@@ -1,7 +1,7 @@

<template>
<view>
<nb-h1>child: {{ value }}</nb-h1>
- <nb-button :on-press="() => $emit('update:parentValue', ++value)">
+ <nb-button :on-press="() => $emit('update', ++value)">
<nb-text>increment</nb-text>
</nb-button>
</view>


sample-fail-emit.jpg

やはりダメです。そもそもイベントが親に伝わっていないようです。


関連issueと開発者の見解

詰まったので本家のドキュメントやリポジトリを見に行くと、既にissueが上がっていました。


Right now \$emit works only if you have \$on in the same component within scripts which listens to the event fired by $emit


とのことで、現在の仕様ではコンポーネントが違うと$emit()を使えないようです。

このissueは現在閉じられており、対処されるかは未定となっています。「回避策が存在するからそこまで深刻じゃないよ(意訳)」というのが理由です。


対策:関数を子に渡して実行させる

issueで紹介されていた回避策は、$emit()を諦めて子に更新用の関数を渡し、実行してもらうというものです。

update()という関数を別途渡してみます。



差分


src/parent.vue

@@ -2,7 +2,7 @@

<nb-root>
<nb-body :style="{ justifyContent : 'center' }">
<nb-h1>parent: {{ value }}</nb-h1>
- <child :parentValue.sync="value" />
+ <child :parentValue.sync="value" :update="val => value = val" />
</nb-body>
</nb-root>
</template>


src/child.vue

@@ -1,7 +1,7 @@

<template>
<view>
<nb-h1>child: {{ value }}</nb-h1>
- <nb-button :on-press="() => $emit('update:parentValue', ++value)">
+ <nb-button :on-press="() => update(++value)">
<nb-text>increment</nb-text>
</nb-button>
</view>
@@ -12,6 +12,6 @@ export default {
data() {
return { value: 0 }
},
- props: { parentValue: Number }
+ props: { parentValue: Number, update : Function }
}
</script>


sample-ok.gif

できました!


結論

Vue Nativeでは、子コンポーネントから親コンポーネントに$emit()できません。

イベントを処理する関数を子へ渡し、$emit()の代わりに実行させることで、双方向バインディングが実現できます。

しかし、せっかく.syncで簡潔に書けるのに、バインドするものが2つに増えてしまうのは残念でなりません。関数で親にデータを渡すのも、普段のVue.jsの書き方と違っていて戸惑います。

Vue Nativeでは、Vuexを使うこともできます(公式のサンプル)。いくつもコンポーネントを分ける規模になるなら、初めからVuexを導入するのもいいかもしれません。


最後に

Vue Nativeには今回述べたような問題点もあるとはいえ、Vue.jsの知識があれば今すぐにでもネイティブアプリが作れてしまうのは大きな魅力です。

「iOSアプリやってみたいけどmac持ってない……」「ネイティブアプリの環境作るの面倒だな……」なんて人にはぜひ触れていただきたい技術だと思います。

明日の記事は@morifuji551さんの「コマンドプロンプトでjavaは通るが,javacが通らない」です。僕はJava初心者なので低みの見物をさせて頂きますね :wink: