6
7

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 3 years have passed since last update.

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

Last updated at Posted at 2021-06-04

これはなに

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

環境

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

検証するソース

親コンポーネント

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

といった感じで5つのオブジェクトの中身を書き換えてみます。
画面にするとこんな感じ。
左が子供、右が親です。
子供側でボタン押したり、フォーム入力するなどのアクションをとると親側がどう変化するかをそれぞれ見ていきます。

スクリーンショット 2021-06-04 22.27.10.png

第一章 1階層のオブジェクト

子コンポーネント

<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
それぞれボタンやフォーム入力した際にはコンソールで中身を出すようにしました。
結果は

オブジェクトに直接オブジェクトを代入

Set operation on key "obj1" failed: target is readonly.
と怒られました。propsの値を直接書き換えるのはルール違反です。中身も変わりませんでした。

オブジェクトの中身に代入

書き換わった文字列がコンソールに表示されました!
またオブジェクト自体ではなく中身の変更の場合エラーはでません。
しかしながら親側の文字列は初期値のままです。
そもそもobj1.nameはリアクティブでもなく、双方向のバインディングもなされていないので親は微動だにしません。
ですがエラーは出ずにに子供側の中身は変わりました。危ないですね〜。

第二章 2階層のオブジェクト

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

    const obj2 = {
      name: {
        firstName: "obj2.name.firstNameの初期値",
      },
    };

結果は1階層となにも変わらず。(そりゃそうか)

第三章 リアクティブな要素を持つオブジェクト

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

const obj3 = {
  gender: ref("obj3.gender.valueの初期値"),
};

子コンポーネント

<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

親も書き換わった!

エラーなし、そして親側の中身も書き換わりました!
しかし正直「え、これでいいの?」という感じ。
なんとなくルール違反をしている気がするのです。
子コンポーネントでpropsの中身をv-modelするのはいささか不安が残ります。

第四章 computedを使う

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

親コンポーネント

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

const updateText = (val: string) => {
  obj4.text.value = val;
};

子コンポーネント(Obj4.vue)

<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

問題なし

挙動的には問題がなく、子供で直接書き換わるような記述もないので安心♨︎

第五章 Vue3ではv-modelが使える

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

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

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

<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使う方が好きだなぁと思う今日この頃です。
慣れですかね。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?