Vue.js における Props の受け渡しと Emit によるイベント発火の流れ

## 1. はじめに
現在、Vue2 から Vue3 へのマイグレーションに携わる中で、
「そもそも props とは？emits とは？」といった基礎でつまずいてしまいました。
そこで今回は、`v-model` を使用したモーダルのコード例をもとに、流れを再確認してみようと思います。

:::note info 
本記事では、従来のOptions API で書いたコードを例に紹介します。 
:::

ここでは、親子間での `props` の受け渡しと、子コンポーネントから親コンポーネントへの `emit` によるイベント発火を、具体的なコード例を通してまとめます。

## 2. コンポーネントの構成

今回の例は 2 つのコンポーネントから成り立っています。

- **親コンポーネント**  
    ボタンをクリックすると `visible` が `true` になり、その状態が子コンポーネントへ渡す
    
- **子コンポーネント（モーダル）**  
    親から渡された `visible` をもとにモーダルを表示し、モーダル内部でのクローズ操作によって親へ状態変更を通知する
    

### 親コンポーネントのコード例


```vue
<template>
  <!-- ボタンをクリックすると、visible が true に更新され、子に伝わる -->
  <button @click="visible = true">Open Modal</button>
  <ChildComponent v-model:visible="visible" />
</template>

<script>
export default {
  data() {
    return {
      // モーダルの表示/非表示管理
      visible: false,
    }
  },
}
</script>
```

### 子コンポーネントのコード例


```vue
<template>
  <div v-if="localVisible" class="modal-overlay">
    <div class="modal-content">
      <p>Modal Content Here</p>
      <!-- クローズボタンでモーダルを閉じる -->
      <button @click="closeModal">Close</button>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    // 親コンポーネントから受け取る状態
    visible: {
      type: Boolean,
      default: false,
    },
  },
  emits: ['update:visible'],
  computed: {
    // computed プロパティで props をラップして双方向データバインディングを実現
    localVisible: {
      get() {
        return this.visible;
      },
      set(value) {
        this.$emit('update:visible', value);
      },
    },
  },
  methods: {
    closeModal() {
      // クローズボタン押下時、親にモーダルを閉じるよう通知
      this.$emit('update:visible', false);
    },
  },
}
</script>

<style scoped>
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5); 
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.modal-content {
  background: #fff;
  padding: 20px;
  border-radius: 4px;
  min-width: 300px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
</style>
```


https://play.vuejs.org/#eNp9VW9PHEUY/yrjqoE23B2WmpjrlWhJEzXRGjV94/pi2Z27G9jb3ezOHoeEhN2VtiAI0lqLsTYtSJEKNakxSMF+mOHu4FW/gs/M7L87bPlzmXvmmWd+z5/fb2aUDxyn2PSxUlYqFDccU6N4VLVUWnmjUEAs+oWFL1j0jIVrLHzKoh0WRXwRrLPwOxZss7mgSTwybmLEgr3u4t+deTAvIer63PKEBTssXASv9u4qC5/xSNF9Fh2yaFmsb4FPZ/8hC35k4RKERIWCvH3cp9S20Pu6SfTJy6qS3HJZhFaV0WsOttAntqGZlZJ0HkUIpcibhYZtYBMB8OOjFyzYYMHWydYOQOZIUqjhWntlqXP3n/bqD4DkdG6js3qQYhirE9MYsxuObWGLJiHLMZQMlKqgEpyolNICchxWxdNd4lDYIRDCpagvXtW1G2igWOo182YM8OtxSxwycFXzTYpmeHKQIdITT6+MZqQJ9YWW1tkhHkalhka1wXM8QFwg/uti6rtWbMzMpRKCKrWDv9ISvTy8xaJHomVzLIKG7p083O5uHrDo+en9X+X65eFCAgShuChlVNVMDw/1hJ9NlgBOrNAsr1tcKGVIoZ5uW1VSK054tgUzKRJUFZ4zMbF7zaHEtjxVSVNXFc007amPhY2PBiQt7Xod65P/Y5/wWtymKp+52MNuE9qX7lHNrWEqt69+8SluwTrdhO77vNmv2fwce7bpc4zS7YpvGQA75yfQfiTmgVi1L72rLYotL0lKzDZ48tZxb5gF3tRXpZ7BHSleFOdUaxaqeHai+uiNkKR3CHyGxq6Kzz9YuMmCx9070NhH7fnt46PbwI+TrRss+BYYfLy/297c45zOj0O4djq/3D3ak6SBuAZpAlNIFehh2rpmXk85opua54EZSqWZBbuJXVObBibLbMTBXhcYBSgO5Be7gJMzKiiPxuQW+hC7uFJyMgeZFijVLscYwpgmChY87gd+d4EF91LRiQP0C49u2h4WlwKOMf4l1ZsYeAmQy9zjVU4IenWgn9L8kOPaTsZkyT8WgDIt5HVRclH6pARL2U+nHfh6xbZNrFnxqKHkmoSImSgkn3DZdR+PcOVO9bJ7Z5uPQbQu5Zm74QbhWvPVgO+AlOBE/wa+FkE4NX2KjXwKIicuvSz6nT8Y0U8sgL+tTGujm6I90Bh4FuA6uOsmCzfES/MnV+W9B92Vf2XA/BjlkobJ56qWCU8sabROvGKM8VKyKxPmPx4ca2qmj3vOikNv8UwH+9McQtL9TKx8KRuY1m0j62M2NXmMUBp4fYS68gaLTu+/aipZcJs/sGCZ+7n74Le0169DKjqdIM3w5UVWTiSdhrcUtNbBBlhK56HvZ1l/46w+oPMl1Sr2MDgeY9sjXKBg2kgLGwICtZ0yGhZLE1dhEOV6ihi0XkbvDA+/Lb7XManVYTc1jGv6ZM21QTvLyK2Na4PDQyj+K7577hICuO3lxdO5lc6972F2T5+vnURLnfWnAhzMPfGAfNOAxMQtEVAzSc0qEGAldEgH3cCusE/4HiXV6URoeva+KRDLwC0Bi+OGGopCpTIYP4MJQ/seSXA7OXiSL1d8iSxXPsU3q9WquNLRDAMehTK6MOxI4OO2a2C34GoG8QH6xdjcIFYhruLIcObbKnh1zbCnoNDogtNC78F/f/1G+HTIeeAzMKrM/gcdvBDU


## 3. Props の受け渡し

### 親から子へのデータ伝達

- **親コンポーネント側**  
   `data` オプションで定義した `visible` を管理し、ボタンがクリックされると `visible` の値を `true` に更新
    
- **子コンポーネント側**  
    `props` オプションで `visible` を受け取ることで、親コンポーネントの状態を参照できるようにする
    

## 4. v-model を用いた双方向バインディングの実現

### computed プロパティでのラッピング

- 子コンポーネントでは、親の`visible`を直接変更できないため、`localVisible` という computed プロパティを定義し、以下のように動作させています：
    - **Getter:**  
        親から渡された `visible` をそのまま返し、表示条件（`v-if`）などで使用
    - **Setter:**  
        `localVisible` に値が設定されると、`this.$emit('update:visible', value)` を実行し、親に変更を通知

こうすることで、`<ChildComponent v-model:visible="visible" />` と書いた際に、Vue は内部的に `update:visible` イベントを監視し、親の `visible` を更新します。

## 5. Emit によるイベント発火の流れ

### 子から親への通知

- **閉じる操作の場合:**  
    子コンポーネント内の「Close」ボタンがクリックされると、`closeModal` メソッドが呼び出され、その中で `this.$emit('update:visible', false)` を実行します
    これにより、親コンポーネントに対して「モーダルを閉じる」ように通知します

### 親コンポーネントでの反映

- 親は `v-model:visible` を通じて子の `update:visible` イベントを受け取っているため、`false` が emit されると自動的に `visible` が更新され、モーダルが非表示となります


## 6. まとめ

- **データの一方向伝播:**  
    親から子へは `props` を使って一方向に状態（この例では `visible`）が渡される
    
- **子から親への通知:**  
    子コンポーネントは、`$emit` でイベント（ここでは `update:visible`）を発火し、親に状態変更を通知する
    
- **双方向バインディングの実現:**  
    computed プロパティの getter / setter を設定することで、`v-model` による双方向バインディングが実現
    
## 7.参考

https://ja.vuejs.org/guide/components/props#one-way-data-flow

https://navigate-online-store.com/vue3-props-emit/