Vue.jsで、親コンポーネントからもらった変数を子コンポーネントで更新したいときの対処法

Vue.jsで、すぐ忘れちゃうコンポーネントの記述方法。
とくに**親コンポーネントからもらった変数を子コンポーネントが更新したい場合**の記述方法について備忘録。

この辺のはなしです。https://jp.vuejs.org/v2/guide/components.html

## イントロ

Vue.js は、親コンポーネントから渡された変数を子コンポーネントで更新しようとすると、下記のwarningが出ます。

> [Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "data"

「親コンポーネントから渡された変数を子コンポーネントで更新」っていうコードは具体的にはこんなケースです。

```vuejs:ParentView.vue(親)
<template>
  <div>
    <NgComponent :data="param1"></NgComponent>
    親:[{{ param1 }}]
  </div>
</template>

<script>
import NgComponent from './NgComponent.vue'

export default {
  name: 'HelloWorld',
  components: {
    NgComponent,
  },
  data: () => ({
    param1: 1,
  }),
}
</script>
```

```vuejs:NgComponent.vue(子)
<template>
  <div>
    <button v-on:click="handle">ボタン</button>
    <div>子:{{ data }}</div>
  </div>
</template>

<script>
export default {
  props: {
    data: Number,
  },
  methods: {
    handle() {
      console.log('NGバージョン start')
      console.log(this.data)
      this.data += 10 // 親からもらったパラメタを変更しようとすると、ココで怒られる
      console.log(this.data)
      console.log('NGバージョン end')
    },
  },
}
</script>
```

実務だと「**子コンポーネントに渡した配列に、コンポーネント側がつねに、Firestoreからとってきた最新データを更新し続ける**」なんてケースがありました。

さて、上記のwarningがでるとき、子のコンポーネントでは値が更新されるのですが、親がわには反映されません。対処法として**渡す変数の型を``Object``にする、**つまり

```vuejs:(親)
  data: () => ({
    param1: { p1: 1 },
  }),
```

```vuejs:(子)
<script>
export default {
  props: {
    data: Object,
  },
  methods: {
    handle() {
      console.log('NGバージョン start')
      console.log(this.data)
      this.data.p1 += 10
      console.log(this.data)
      console.log('NGバージョン end')
    },
  },
}
</script>
```

って対応もできるっぽいのですが、そもそもVue.jsではコンポーネント間での結合を疎にするために「**子でのコンポーネントの値の変更**」を許可(推奨?)していないようです。なのでそれをやりたければ、

- 親から子コンポーネント → props プロパティ経由で参照を渡す
- 子から親コンポーネント → ``$emit`` というイベント機構で親へ通知(そのさい、変更後データも渡す) 

とする必要があるようです。
ってことで**子のコンポーネントでのデータ更新を、親へ伝播する方法**を整理しました。

## ``$emit``による通知

親からもらう変数は直接変更は出来ないのですが、先の``$emit``を用いることで

- 親からは``value``という属性で、親がわの変数を受け渡す
- 子コンポーネント側で``computed``な変数(下記コード中の``localParam``)を定義
- ``localParam``の``getter/setter``として、親からもらった変数を**操作**(更新じゃないよ)するように定義。
- **操作**とは具体的には、``setter``で(変更はダメなので) ``$emit``を使って``input``という名前のイベントを発生させ、**親に変更した値を通知**します。
- 親は``input``イベントを監視して、そこから取得できる``$event``変数経由で変更された値をもらって、もとのパラメタに反映させます

というやり方。具体的には下記の通り。

```vuejs:ParentView.vue(親)
<template>
  <div>
    <h2>対応策1(OkComponent1.vue)</h2>
    <OkComponent1 :value="param1" @input="param1 = $event"></OkComponent1>
    [{{ param1 }}]
  </div>
</template>

<script>
import OkComponent1 from './OkComponent1.vue'

export default {
  name: 'HelloWorld',
  components: {
    OkComponent1,
  },
  data: () => ({
    param1: 1,
  }),
}
</script>
```

```vuejs:OkComponent1.vue(子)
<template>
  <div>
    <button v-on:click="handle">ボタン</button>
    <input type="text" v-model="localParam" />
    <div>{{ localParam }}</div>
  </div>
</template>

<script>
// 値をこちらのコンポーネントで書き換える場合
export default {
  props: {
    value: Number,
  },
  computed: {
    localParam: {
      get: function() {
        return this.value
      },
      set: function(value) {
        this.$emit('input', value) // おやでは @input に書いたメソッドがよばれる。引数にvalue
      },
    },
  },
  // data: vm => ({}),
  methods: {
    handle() {
      console.log('v-modelバージョン start')
      console.log(this.localParam)
      this.localParam += 10 // 実際は、うえのsetterが呼ばれてemitされる
      console.log(this.localParam)
      console.log('v-modelバージョン  end')
    },
  },
}
</script>

```

このように**``computed``で定義された変数のアクセッサ(getter/setter)を上書きする**ことで、親からもらった変数を子がわで**ふつうに操作する感覚**で``$emit``することができます。

子での変更は ``input``というイベントで通知されるので、親がわはそれを監視して値を取り出し、再度、該当する変数に代入すればよいということですね。


### ``v-model``をつかう

上記の対応ですが、親がわのコード``<OkComponent1 :value="param1" @input="param1 = $event"></OkComponent1>``は、じつは``<OkComponent1 v-model="param1"></OkComponent1>``という簡略記法があるようです。このsyntactic sugarを使うと、親は下記のように書くことが出来ます。。

```vuejs:(親)
<template>
  <div>
    <h2>対応策1(のsyntactic sugarバージョン)(OkComponent1.vue)</h2>
    <OkComponent1 v-model="param1"></OkComponent1>
    [{{ param1 }}]
  </div>
</template>

<script>
import OkComponent1 from './OkComponent1.vue'

export default {
  name: 'HelloWorld',
  components: {
    OkComponent1,
  },
  data: () => ({
    param1: 1,
  }),
}
</script>
```

よくでてくる**``v-model``でパラメタを渡せるコード**になりました。

まとめると``v-model`` の方式は**``value``という属性で子に変数を渡して、``input``というイベント名で通知を待つ**よう、あらかじめつくられているようですね。。

参考: [コンポーネントで v-model を使う](https://jp.vuejs.org/v2/guide/components.html#%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%81%A7-v-model-%E3%82%92%E4%BD%BF%E3%81%86)


### 複数の変数を渡したい場合

``v-model``は一つの変数を``value`` で渡すことができましたが、複数パラメタを渡したい場合もあります。そのために``.sync``修飾子というのがあるんですが、まずはそういったsyntactic sugarを用いないやり方でやると、下記の通りになります。


```vuejs:ParentView.vue(親)
<template>
  <div>
    <h2>複数パラメタを渡すパタン(.sync未使用パタン)(OkComponent2.vue)</h2>
    <OkComponent2
      :param="param1"
      :paramObj="param2"
      @update:param="param1 = $event"
      @update:paramObj="param2 = $event"
    ></OkComponent2>
    [{{ param1 }}] [{{ param2 }}]
  </div>
</template>

<script>
import OkComponent2 from './OkComponent2.vue'

export default {
  name: 'HelloWorld',
  components: {
    OkComponent2,
  },
  data: () => ({
    param1: 1,
    param2: { param1: 2 },
  }),
}
</script>
```

```vuejs:OkComponent2(子)
<template>
  <div>
    <button v-on:click="handle">ボタン</button>
    <input type="text" v-model="localParam" />
    <div>{{ localParam }}</div>

    <input type="text" v-model="localParamObj.param1" />
    <div>{{ localParamObj.param1 }}</div>
  </div>
</template>

<script>
// 値をこちらのコンポーネントで書き換える場合
export default {
  props: {
    param: Number,
    paramObj: Object,
  },
  computed: {
    localParam: {
      get: function() {
        return this.param
      },
      set: function(value) {
        this.$emit('update:param', value)
      },
    },
    localParamObj: {
      get: function() {
        return this.paramObj
      },
      set: function(value) {
        this.$emit('update:paramObj', value)
      },
    },
  },
  // data: vm => ({}),
  methods: {
    handle() {
      console.log('syncを使って、子で値を変更パタン start')
      this.localParam -= 1
      this.localParamObj.param1 += 1
      console.log('syncを使って、子で値を変更パタン end')
    },
  },
}
</script>
```

親からは``:param="param1"``,``:paramObj="param2"`` で変数を渡しています。子がわは、それぞれの変数に対して``computed``な変数を定義し、値を操作します。親へ通知するイベントは

```
this.$emit('update:param', value)
this.$emit('update:paramObj', value)
```

としています。先ほどは``input``でしたが、今回は``update``としています(なぜかは後述)。

複数のパラメタをやりとりする場合はコレでOKです。


### そのsyntactic sugar版

```vuejs
<OkComponent2
  :param="param1"
  :paramObj="param2"
  @update:param="param1 = $event"
  @update:paramObj="param2 = $event"
></OkComponent2>
```

は、``.sync`` という簡易的な記法を用いて下記の通り書くことが出来ます。

```vuejs
<OkComponent2 :param.sync="param1" :paramObj.sync="param2"></OkComponent2>
```

こうです。コレで ``param``,``paramObj``で渡される変数を ``update``イベントで待ち受けることになるので、子のコンポーネント側は更新時``update``イベントを発行すればよいわけですね。


超備忘で、駆け足でした。おつかれさまでした。


### 今回のコード

下記からダウンロード出来ます。
https://github.com/masatomix/component-samples.git

いちおうですが、具体的なセットアップは以下の通り。

```console
$ git clone --branch  component001 https://github.com/masatomix/component-samples.git
$ cd component-samples/
$ npm install
$ npm run serve

> component-samples@0.1.0 serve /private/tmp/component-samples
> vue-cli-service serve

...
  App running at:
  - Local:   http://localhost:8080/ 
  - Network: http://192.168.10.24:8080/

  Note that the development build is not optimized.
  To create a production build, run npm run build.
```

上記にアクセス出来ればOKです。



## 関連リンク

- [コンポーネントで v-model を使う](https://jp.vuejs.org/v2/guide/components.html#%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%81%A7-v-model-%E3%82%92%E4%BD%BF%E3%81%86)
- [.sync 修飾子](https://jp.vuejs.org/v2/guide/components-custom-events.html#sync-%E4%BF%AE%E9%A3%BE%E5%AD%90)
- [Vue.jsコンポーネント入門 (5) コンポーネント間のコミュニケーション](https://www.hypertextcandy.com/vuejs-components-introduction-communication-between-components)