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

この記事は[U-Tokyo mech (東京大学機械系) Advent Calendar 2018](https://qiita.com/advent-calendar/2018/utmech)の3日目の記事です。

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

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

---

[Vue Native](https://vue-native.io/)は、有名なJavaScriptフレームワークである[Vue.js](https://jp.vuejs.org/index.html)で**モバイルアプリ（iOS/Android）を作れる**というものです。入門は凄まじく簡単で、説明に従ってコマンドをポチポチすれば（参考：[さっそくVue NativeでHello Worldしてみた](https://qiita.com/taquaki-satwo/items/523f65723cf2bf7b049a)）**数分後には手元のスマホでアプリが動きます**。

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

## 概要
### 環境
```bash
$ 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](https://facebook.github.io/react-native/)があり、圧倒的な人気を誇ります。

Vue Nativeは、[GeekyAnts](https://geekyants.com/)というインドのWeb企業によるJavaScriptフレームワークです。その実態は、React Nativeを**Vue.jsの書き方で使えるようにするラッパー**です。
Vue.js最大の特徴の一つ、[単一ファイルコンポーネント](https://jp.vuejs.org/v2/guide/single-file-components.html)を使うことができ、Vue使いに嬉しいポイントとなっています。

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

React Nativeとの関係については、同じくGeekyAnts製のコンポーネントライブラリである[**NativeBaseのドキュメント**](https://docs.nativebase.io/Components.html#button-def-headref)を見ると雰囲気が分かりやすいでしょう。ReactとVueの書き方を切り替えて見ることができます。

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

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

- [[Vue] 親コンポーネントと子コンポーネントで双方向バインディングする](https://qiita.com/Sa2Knight/items/544b3f157108b96033fe)

親から子に数値を渡し、子のボタンを押すとカウントアップして親に返すサンプルです（[`.sync`修飾子](https://jp.vuejs.org/v2/guide/components-custom-events.html#sync-%E4%BF%AE%E9%A3%BE%E5%AD%90)を利用しています）。

```vuejs: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>
```
```vuejs: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>
```

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

<img alt="sample-fail.gif" src="https://qiita-image-store.s3.amazonaws.com/0/150557/0c5af7ef-8188-e04c-567a-d7a0923e49a7.gif" width=250px>

…増えていませんね。

### 省略せずに書いてみる
`.sync`は、

- 親から子に`v-bind`でデータをバインドする
- 子から親に`$emit()`したイベントを`v-on`で受け取る

というパターンを短く書ける構文です。
これを略さずに書いた上で、イベントが発生したがどうかも表示させてみます。

<details>
<summary>差分</summary><div>

```diff: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 }
 }
```
```diff: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>
```
</div></details>

<img alt="sample-fail-emit.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/150557/f068c037-700c-0c72-0307-8e9b07bafbec.jpeg" width=250px>

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

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

- [$emit() doesn&#39;t work · Issue #25 · GeekyAnts/vue-native-core](https://github.com/GeekyAnts/vue-native-core/issues/25)

> 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で紹介されていた回避策](https://github.com/GeekyAnts/vue-native-core/issues/25#issuecomment-412457861)は、`$emit()`を諦めて子に更新用の関数を渡し、実行してもらうというものです。

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

<details>
<summary>差分</summary><div>

```diff: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>
```
```diff: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>
```
</div></details>

<img alt="sample-ok.gif" src="https://qiita-image-store.s3.amazonaws.com/0/150557/acdf580b-e90a-4a83-b1f4-936584357f85.gif" width=250px>

できました！

## 結論
Vue Nativeでは、子コンポーネントから親コンポーネントに`$emit()`できません。
**イベントを処理する関数を子へ渡し、`$emit()`の代わりに実行させる**ことで、双方向バインディングが実現できます。

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

Vue Nativeでは、Vuexを使うこともできます（[公式のサンプル](https://github.com/GeekyAnts/vue-native-starter-app)）。いくつもコンポーネントを分ける規模になるなら、初めからVuexを導入するのもいいかもしれません。

## 最後に
Vue Nativeには今回述べたような問題点もあるとはいえ、Vue.jsの知識があれば今すぐにでもネイティブアプリが作れてしまうのは大きな魅力です。
「iOSアプリやってみたいけどmac持ってない……」「ネイティブアプリの環境作るの面倒だな……」なんて人にはぜひ触れていただきたい技術だと思います。

明日の記事は@morifuji551さんの「[コマンドプロンプトでjavaは通るが，javacが通らない](https://qiita.com/morifuji551/items/c07815c9905ec21db631)」です。僕はJava初心者なので低みの見物をさせて頂きますね :wink: