Vue.jsで親子間の双方向バインディングをいろいろ試してみた

## これはなに
Vue.jsを使う上で、親で定義した要素を子コンポーネントに渡し、フォームに反映させるのはよくあること。
で、その要素を書き換える方法がいろいろあるなと思ったので少しまとめてみました。
badな例もあります。

## 環境
Vue3
typescript 4.3.2
Composition APIを使用します。

## 検証するソース
親コンポーネント

```vuejs
<template>
  <div class="child-components">
    <!-- obj1.name 1階層だとどうなる？ -->
    <obj1-vue :obj1="obj1" />
    <br />----------------------------------
    <!-- obj2.name.firstName 2階層だとどうなる？ -->
    <obj2-vue :obj2="obj2" />
    <br />----------------------------------
    <!-- obj3.gender.value リアクティブな要素を持つオブジェクト -->
    <obj3-vue :obj3="obj3" />
    <br />----------------------------------
    <!-- obj4.gender.value emitで書き換えよう -->
    <obj4-vue :obj4="obj4" @update:text="updateText" />
    <br />----------------------------------
    <!-- obj5.description.value Vue3からのv-model -->
    <obj5-vue v-model:description="obj5.description.value" />
  </div>

  <div class="parent-content">
    <h3>親側の値</h3>
    <p>{{ obj1.name }}</p>
    <p>{{ obj2.name.firstName }}</p>
    <p>{{ obj3.gender.value }}</p>
    <p>{{ obj4.text.value }}</p>
    <p>{{ obj5.description.value }}</p>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from "vue";
import Obj1Vue from "./components/Obj1.vue";
import Obj2Vue from "./components/Obj2.vue";
import Obj3Vue from "./components/Obj3.vue";
import Obj4Vue from "./components/Obj4.vue";
import Obj5Vue from "./components/Obj5.vue";

export default defineComponent({
  name: "App",
  components: {
    Obj1Vue,
    Obj2Vue,
    Obj3Vue,
    Obj4Vue,
    Obj5Vue,
  },
  setup() {
    const obj1 = { name: "obj1.nameの初期値" };
    const obj2 = {
      name: {
        firstName: "obj2.name.firstNameの初期値",
      },
    };
    const obj3 = {
      gender: ref("obj3.gender.valueの初期値"),
    };

    const obj4 = {
      text: ref("obj4.textの初期値"),
    };
    const updateText = (val: string) => {
      obj4.text.value = val;
    };

    const obj5 = {
      description: ref("obj5.descriptionの初期値"),
    };

    return {
      obj1,
      obj2,
      obj3,
      obj4,
      updateText,
      obj5,
    };
  },
});
</script>
```
といった感じで５つのオブジェクトの中身を書き換えてみます。
画面にするとこんな感じ。
左が子供、右が親です。
子供側でボタン押したり、フォーム入力するなどのアクションをとると親側がどう変化するかをそれぞれ見ていきます。

![スクリーンショット 2021-06-04 22.27.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358229/4e28f91a-d48d-2eb1-e6c6-2598129089f1.png)

### 第一章 1階層のオブジェクト
子コンポーネント

```vuejs
<template>
  <div class="obj1">
    <button @click="submit">空objectを代入</button>
    <button @click="submit1">文字列を代入</button>
    <input v-model="obj1.name" @input="input" />
  </div>
</template>

<script lang="ts">
import { defineComponent, PropType } from "vue";

type Obj1 = {
  name: String;
};

export default defineComponent({
  name: "HelloWorld",
  props: {
    obj1: {
      type: Object as PropType<Obj1>,
    },
  },
  setup(props) {
    const submit = () => {
      props.obj1 = {};
      console.log(props.obj1);
    };

    const submit1 = () => {
      props.obj1.name = "中身変更！";
      console.log(props.obj1);
    };

    const input = () => {
      console.log(props.obj1);
    };

    return {
      submit,
      submit1,
      input,
    };
  },
});
</script>
```
![obj1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358229/685f757b-06fd-db10-28ec-d2fd74a809b0.gif)
それぞれボタンやフォーム入力した際にはコンソールで中身を出すようにしました。
結果は
#### オブジェクトに直接オブジェクトを代入
`Set operation on key "obj1" failed: target is readonly.`
と怒られました。propsの値を直接書き換えるのはルール違反です。中身も変わりませんでした。
#### オブジェクトの中身に代入
書き換わった文字列がコンソールに表示されました！
またオブジェクト自体ではなく中身の変更の場合エラーはでません。
しかしながら親側の文字列は初期値のままです。
そもそも`obj1.name`はリアクティブでもなく、双方向のバインディングもなされていないので親は微動だにしません。
ですがエラーは出ずにに子供側の中身は変わりました。危ないですね〜。

### 第二章 2階層のオブジェクト
1階層とやってることは同じで、対象のオブジェクトを1階層深くしてみました。

```js
    const obj2 = {
      name: {
        firstName: "obj2.name.firstNameの初期値",
      },
    };
```
結果は1階層となにも変わらず。（そりゃそうか）

### 第三章 リアクティブな要素を持つオブジェクト
第一章ではそもそもリアクティブなオブジェクトを渡さなかったので、今度はリアクティブにしてみます。

```js
const obj3 = {
  gender: ref("obj3.gender.valueの初期値"),
};
```
子コンポーネント

```vuejs
<template>
  <div class="obj3">
    <button @click="submit">文字列を代入</button>
    <input v-model="obj3.gender.value" @input="input" />
  </div>
</template>

<script lang="ts">
import { defineComponent, PropType, Ref } from "vue";

type Obj3 = {
  gender: Ref<string>;
};

export default defineComponent({
  name: "Obj3",
  props: {
    obj3: {
      type: Object as PropType<Obj3>,
    },
  },
  setup(props) {
    const submit = () => {
      props.obj3.gender.value = "中身変更！";
      console.log(props.obj3);
    };

    const input = () => {
      console.log(props.obj3);
    };

    return {
      submit,
      input,
    };
  },
});
</script>
```
さてどうなるのか
![obj3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358229/cb0887f8-ae48-3b19-a472-10903be93b3d.gif)
#### 親も書き換わった！
エラーなし、そして親側の中身も書き換わりました！
しかし正直「え、これでいいの？」という感じ。
なんとなくルール違反をしている気がするのです。
子コンポーネントでpropsの中身をv-modelするのはいささか不安が残ります。

### 第四章 computedを使う
第三章の処理に不安が残るのでcomputedでgetter/setterを用意し、それをv-modelします。
set時は`emit`を使い親コンポーネントで中身を書き換えます。

親コンポーネント

```js
const obj4 = {
  text: ref("obj4.textの初期値"),
};

const updateText = (val: string) => {
  obj4.text.value = val;
};
```
子コンポーネント（Obj4.vue）

```vuejs
<template>
  <div class="obj4">
    <button @click="submit">文字列を代入</button>
    <input v-model="text" @input="input" />
  </div>
</template>

<script lang="ts">
import { computed, defineComponent, PropType, Ref } from "vue";

type Obj4 = {
  text: Ref<string>;
};

export default defineComponent({
  name: "Obj4",
  props: {
    obj4: {
      type: Object as PropType<Obj4>,
    },
  },
  setup(props, { emit }) {
    const text = computed({
      get: () => props.obj4.text.value,
      set: (val: string) => emit("update:text", val),
    });

    const submit = () => {
      text.value = "中身変更！";
      console.log(props.obj4);
    };

    const input = () => {
      console.log(props.obj4);
    };

    return {
      submit,
      input,
      text,
    };
  },
});
</script>
```
![obj4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358229/cd57ec50-eda0-98d0-1dc8-1a7f908f0e5d.gif)
#### 問題なし
挙動的には問題がなく、子供で直接書き換わるような記述もないので安心♨︎

### 第五章 Vue3ではv-modelが使える
そもそもVue2までは`.sync`を利用して親子間の双方向バインディングを実現することができました。
しかしVue3で`.sync`が廃止され、そのかわりに`v-model`が使えるようになりました。

その部分だけ抜粋した親コンポーネント

```vuejs
<template>
  <div class="child-components">
    <!-- obj5.description.value Vue3からのv-model -->
    <obj5-vue v-model:description="obj5.description.value" />
  </div>
</template>

<script lang="ts">
export default defineComponent({
  name: "App",
  components: {
    Obj5Vue,
  },
  setup() {
    const obj5 = {
      description: ref("obj5.descriptionの初期値"),
    };

    return {
      obj5,
    };
  },
});
</script>
```
子コンポーネント（Obj5.vue）

```vuejs
<template>
  <div class="obj5">
    <button @click="submit">文字列を代入</button>
    <input
      :value="description"
      @input="$emit('update:description', $event.target.value)"
    />
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent({
  name: "Obj5",
  props: {
    description: {
      type: String,
    },
  },
  setup(props, { emit }) {
    const submit = () => {
      emit("update:description", "中身変更！");
      console.log(props.description);
    };

    return {
      submit,
    };
  },
});
</script>
```
親側で本来定義する`@update:description="obj5.description.value = $event"`の役割をv-modelが担ってくれているようです。

## 総じて
Vue触りたての時とかはこの辺ハマりポイントだったりしますし、
Vue3、Composition APIが出てきて書き方のバリエーションが増えたように思います。
`v-model`もいいけど、暗黙的な感じもあるのでcomputed使う方が好きだなぁと思う今日この頃です。
慣れですかね。