😽

Vue props 親→子へのデータ渡し

2024/12/04に公開

今回はpropsで親コンポーネントから子コンポーネントにデータを渡す方法を解説していきます。

まず、親コンポーネント(App.vue)とcomponentsフォルダに子コンポーネント(ShowCount)コンポーネントを準備して、それぞれ例として書きます。

// App.vue
<script setup>
import { ref } from 'vue'
import ShowCount from '@/components/ShowCount.vue'

const count = ref(0)
</script>

<template>
 <ShowCount />
</template>

//ShowCount.vue
<template>
  <p>count:</p>
</template>


現在はこのように設定し、countの右側に親コンポーネントで定義したcountを子コンポーネント(ShowCount)に渡したいとします。
App.vueの <ShowCount />に適当な属性と値に渡したいデータを入れます。

// App.vue
 <script setup>
import { ref } from 'vue'
import ShowCount from '@/components/ShowCount.vue'

const count = ref(0)
</script>

<template>
  <ShowCount :nyan="count" />
</template>

で、:nyanというのは親から子にデータを渡しているんだということを明示的に示す必要があります。
 そのやり方は子コンポーネントの <script setup>にdefineProps()
という関数を使用します。これはimport必要ありません。
この関数に親コンポーネントに配置した属性の名前を入れます。

//ShowCount.vue
<script setup>
defineProps(['nyan'])
</script>

<template>
  <p>count:</p>
</template>

ここで渡された属性はデータを渡すものなんだという認識になります。
このデータを渡すための属性の名前を『props』と言うのでdefineProps()もpropsを定義するということですね。
 defineProps()で取ってきた属性名をこの子コンポーネントで使用することができるので<p>count:</p>に埋め込んでみます。

//ShowCount.vue
<script setup>
defineProps(['nyan'])
</script>

<template>
  <p>count:{{ nyan }}</p>
</template>

このnyanを子コンポーネントの<script setup>でそのまま使おうとしても使えないので、このdefinePropsの返り値がリアクティブオブジェクトとして使用できるので親から渡されたデータをconst propsとして定義します。

//ShowCount.vue
<script setup>
const props = defineProps(['nyan'])
console.log(props.nyan)
</script>

<template>
 <p>count:{{ nyan }}</p>
</template>

こうすることによって親から渡されたデータを使用することができます。

1.親コンポーネントで子に渡すデータ(値)を作る。
2.子コンポーネントタグに渡すデータの属性名を作り、値を入れる。
3.子コンポーネントにdefinePropsを設定し、親で作った属性名を入れる。
4.definePropsをリアクティブデータにするためにpropsを作る。
という流れですね。(逆でもいいですが)

で、この場合countはref関数なので<button>を作りカウントもできるように設定します。

//ShowCount.vue
<script setup>
import { ref } from 'vue'
import ShowCount from '@/components/ShowCount.vue'

const count = ref(0)
</script>

<template>
  <ShowCount :nyan="count" />
  <button @click="count++">+1</button>
</template>

こうするともちろん、ボタンを押したら再レンダリングされて最新のデータが子コンポーネントに反映されてcountが増えていきます。

このpropsもリアクティブオブジェクトになっているのでconsole.log(props)で見てみると、

なっていて、もちろん親コンポーネントが更新されたら子コンポーネントのpropsも更新されるのでwatch()などでも使えます。
(普通のリアクティブデータとして使用可能です)
注意点ですが、propsはリードオンリーなので子コンポーネントでは変更することができません。

//ShowCount.vue
<script setup>
const props = defineProps(['nyan'])
console.log(props)
props.nyan = 'nyaaan'
</script>

<template>
  <p>count:{{ nyan }}</p>
</template>

props.nyan = 'nyaaan'この変更はエラーが出ます。
なのでpropsは親コンポーネントからデータを引き取るだけの一方通行というのを覚えておきましょう。
 データの変更は親コンポーネントでする必要がありますので見やすくて保守性のあるコードになります。

propsには複数入れることもできて、例えばpropsにmooを追加します。

//ShowCount.vue
<script setup>
defineProps(['nyan', 'moo'])
</script>

<template>
  <p>count:{{ nyan }}</p>
  <p>{{ moo }}</p>
</template>

親コンポーネントのタグにも追加します。

<ShowCount :nyan="count" moo="moo" />

こうすると反映されています。

  moo="moo"このようにv-bindがない場合は文字列を渡すことになっているので右側を変えると文字が変わります。

<ShowCount :nyan="count" moo="Baw wow" />

ここはオブジェクトとして書くこともできます。

  <ShowCount v-bind="{nyan:count, moo:'Baw wow'}" />

前回、コンポーネントタグの属性を付けて子コンポーネントの外側のタグが一つだけの場合は属性継承されるという解説しましたが、definePropsでつけたものは継承されないのでそれも覚えておきましょう。

次にpropsを使った分割代入です。
下記のように返り値に分割代入で書くことは可能です。

const { nyan } = defineProps(['nyan'])

分割代入できる仕組みですが、JavaScriptにおいて分割代入するということはどういうことかというと、上のコードと全く同じものを書くと

<script setup>
const { nyan } = defineProps(['nyan'])
//const nyan = defineProps(['nyan']).nyan
</script>

コメントアウトしたものと上の分割代入は同じ意味になります。
そしてこのdefinePropsはrefオブジェクトなので今、nyanには0が入っているので0が表示されるだけです。
 watchEffectを使って動きを見てみましょう。

<script setup>
import { watchEffect } from 'vue'
//const nyan = defineProps(['nyan']).nyan
const { nyan } = defineProps(['nyan'])
watchEffect(() => {
 console.log(nyan)
})
</script>

<template>
 <p>count:{{ nyan }}</p>
</template>

これでもnyanはリアクティビティシステムを損なわないようなシステムになっています。
これはVueが内部的に分割代入を自動的にpropsに置き換えて

<script setup>
import { watchEffect } from 'vue'
//const nyan = defineProps(['nyan']).nyan
const props = defineProps(['nyan'])
watchEffect(() => {
 console.log(props.nyan)
})
</script>

このような形に変換しています。
 簡単に言うと、分割代入で書いたものは自動的に元の形に戻してくれるということです。
なので今の

<script setup>
import { watchEffect } from 'vue'
//const nyan = defineProps(['nyan']).nyan
const { nyan } = defineProps(['nyan'])
watchEffect(() => {
  console.log(nyan)
})
</script>

<template>
  <p>count:{{ nyan }}</p>
</template>

この状態でもVueがいい感じにやってくれます。

 ただ、watchの場合は第一引数に分割代入のnyanを入れてしまうと自動的にpropsが付いて、リアクティブオブジェクトなので.valueも付いてしまいます。なのでwatchの場合は第一引数も関数にして

watch(() => nyan, () => {})

として使いましょう。

<script setup>
import { watch, watchEffect } from 'vue'
//const nyan = defineProps(['nyan']).nyan
const { nyan } = defineProps(['nyan'])
watchEffect(() => {
 console.log(nyan)
})
watch(
 () => nyan,
 () => {
   console.log('watch')
 }
)
</script>


 第一引数に関数で使わなかった場合はエラーが出ます。

次に、バリデーションを利用してpropsに予期しないデータが渡るのを防ぐ方法を解説します。
ESLintにはdefinePropsに予期しないデータを防いでくれるバリデーションという機能があります。
どうするかというと今、definePropsに配列として書いてありますが、それをオブジェクトデータとして書きます。その値に検証するルールを入れることができます。例えば、NumberやStringなど型を入れて定義します。

<script setup>
defineProps({
  nyan: Number
})
</script>

<template>
  <p>count:{{ nyan }}</p>
</template>

この場合だとnyanにhelloなどの文字列を入れると警告文が出ます。

// App.vue
 <ShowCount :nyan="hello" moo="Baw wow" />


 そしてNumberを書いたところにオブジェクトを入れることができて、そこにtype: Numberと入れても同じように設定することができ、オブジェクトにした場合は他にも色々な設定をすることができます。
typeとは別にrequired: trueも設定すると親コンポーネントに設定がなければエラーとなります。

<script setup>
defineProps({
  nyan: {
    type: Number,
    required: true
  }
})
</script>

この場合、nyanのpropsを親コンポーネントで必ず設定する必要があります。

 <ShowCount :nyan="'nyaaan'" moo="Baw wow" />

あとdefaultも設定ができて、もしpropsが親コンポーネントに設置されてなければ defaultの値が出せるというものです。

<script setup>
defineProps({
  nyan: {
    type: Number,
    required: true,
    default: 100
  }
})
</script>


 今、required: trueとdefault: 100を一緒に書きましたが、本来であれば同時に使うことはありません。
ちなみに、defaultのdefault値はundefinedになります(書いてなければ何も表示されない)ので、

<script setup>
defineProps({
 nyan: {
   type: Number,
 }
})
</script>

<template>
 <p>count:{{ nyan === undefined}}</p>
</template>

こうすると

 trueになります。
 もし親コンポーネントで明示的にundefinedをつけてpropsでdefaultを設定した場合

//App.vue
<ShowCount :nyan="undefined" moo="Baw wow" />
<script setup>
defineProps({
  nyan: {
    type: Number,
    default: 100
  }
})
</script>

<template>
  <p>count:{{ nyan }}</p>
</template>

この場合、propsの100が適用されます。

undefinedはそのpropsが何も指定されていない時、もしくはundefinedと指定されている時に使われます。

これが大体のバリデーションですが、プロジェクトが大きくなって間違えてpropsに違うデータの種類を入れたり指定し忘れたりした時もすぐに警告で教えてくれますので早期発見ができて非常に便利になります。
 ESLintもtypeが無ければなるべくtypeを付けるようにしましょうとエラーが出ますし、defaultかrequiredどちらか無いとこれもエラーになります。
 propsを定義するときはなるべく詳細まで定義するのがいいでしょう。

propsのバリデーションについてもう少し詳しく見ていきます。
バリデーション付きのpropsを複数書くには

<script setup>
defineProps({
  nyan: {
    type: Number,
    default: 100
  },
  moo: {
    type: Number,
    default: 100
  }
})
</script>

このように繋げて書いて複数指定できます。
また、typeのところは下記のように配列を使って複数指定することもできます。

defineProps({
  nyan: {
    type: [Number,String],
    default: 100
  },
})

NumberかStringどちらかであればOKということになります。
他には、Boolean(真偽値), Array(配列), Object(オブジェクト), Function(関数), Symbol, Date(日付), CustomClass(自身で作るclassはインスタンスであればOK)があります。

あと、defaultのところは関数を指定することもできます。

defineProps({
  nyan: {
    type: Function,
    default: function () {}
  }
})

この時はtypeがFunctionの時だけ関数として扱われますが、それ以外の型だと関数がすぐに実行されてdefaultに値が入ります。

defineProps({
 nyan: {
   type: [Number],
   default: function () {
     return 100
   }
 }
})

親コンポーネントには指定を外します。

//App.vue
<ShowCount moo="Baw wow" />


 こうするとdefaultの関数の返り値が指定されているのがわかります。
こうした関数を使った指定はする必要はないのですが、JavaScriptの制約上、defaultの値がオブジェクトもしくは配列の時だけこの関数を使った指定の仕方を行う必要があります。
 なので、下記のように直接defaultの値を指定することはそれがオブジェクトが配列で無ければできません。

defineProps({
  nyan: {
    type: [Number],
    default: {}
  }
})

これではエラーになります。
オブジェクトと配列の時だけ必ず関数を作ってその返り値で指定する方法を取る必要があります。
また、defaultのdefaultプロパティはundefinedと言いましたが、 typeをBooleanにしたときはundefinedになりません。falseが出ます。

  <p>count:{{ nyan === false }}</p>

を入れるとtrueになります。

ちょっとややこしいですが、要はnyanに直接falseが入っているということになるので <p>count:{{ nyan }}</p>こう書いた場合はfalseが入っています。
typeがBooleanの時だけはdefaultのdefault値がfalseというのを覚えておきましょう。
 ちなみに、ESLintではtypeがBooleanの時だけrequiredとdefaultがなかったとしてもエラーは出ません。
また、typeがBooleanのpropsを親のコンポーネントでv-bind使わずに静的に指定する時はHTMLのBoolean属性のように

  <ShowCount nyan moo="Baw wow" />

nyanという属性名を書くだけでもこれはtrueを指定したのと同じ意味になって、無い場合はfalseになります。
 もちろん、v-bindで動的に書く場合はちゃんと値を書く必要はありますし、trueを書いたらtrueに、falseを書いたらfalseになるのでv-bindを使うときは普通のpropsと同じです。

あと、propsのところはもう一つvalidatorというメソッドを書くこともできます。これは返り値でfalseを返すと警告が出るようになっていて、trueを返すと警告は出なくなります。

defineProps({
  nyan: {
    type: Boolean,
    validator: function () {
      return true
    }
  }
})

このvalidatorは引数に親から渡される値、例えば:nyan="count"とすると0とか入るようになりますし、無ければdefaultの値が入るようになっています。
例えば、下記のように渡された値が0もしくは1の時はOKでそれ以外はダメみたいな詳細なバリデーションを書くことがこのvalidatorメソッドを使うことが可能になります。

defineProps({
  nyan: {
    type: Number,
    required: true,
    validator: function (value) {
      return value === 0 || value === 1
    }
  }
})

こうすると2以上は警告が出ます。

あとはJavaScriptの話になりますがメソッドを下記のように省略して書くこともできます。

defineProps({
  nyan: {
    type: Number,
    required: true,
    validator(value) {
      return value === 0 || value === 1
    }
  }
})

こんな感じでvalidatorを設定できます。特になくてもESLintでエラーは出ませんが、詳細にバリデーションが設定できるので活用していきましょう。

このdefinePropsの引数の中では外部のデータは一切使用することができません。例えば外部にconst isRequired = trueを設定し、defineProps内で使おうとしてもエラーが出るので注意です。

最後にpropsの命名規則について解説します。
propsが複数単語になっている場合、子コンポーネントではキャメルケースを使うのが一般的です。(totalScoreみたいな感じです)
 親コンポーネントで属性として使用する時はキャメルケースもOKですがケバブケースも可能となります。(total-scoreみたいな感じです)どちらがいいのかというと、VueはHTMLの書き方で統一したいみたいなのでケバブケースが推奨されていますのでハイフンで区切ったものを使いましょう。

長くなりましたが、propsの説明はこれで終わります。
次回は子コンポーネントから親コンポーネントにデータを渡すemitを解説していきます。

Discussion