98
72

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

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"

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

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>
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にする、**つまり

(親)
  data: () => ({
    param1: { p1: 1 },
  }),
(子)
<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)を定義
  • localParamgetter/setterとして、親からもらった変数を操作(更新じゃないよ)するように定義。
  • 操作とは具体的には、setterで(変更はダメなので) $emitを使ってinputという名前のイベントを発生させ、親に変更した値を通知します。
  • 親はinputイベントを監視して、そこから取得できる$event変数経由で変更された値をもらって、もとのパラメタに反映させます

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

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>
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を使うと、親は下記のように書くことが出来ます。。

(親)
<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 を使う

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

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

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>
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版

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

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

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

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

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

今回のコード

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

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

$ 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です。

関連リンク

98
72
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
98
72

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?