Vue3+Typescriptのネストコンポーネント間データの受け渡し


この記事はWanoグループ [Advent Calendar 2020](https://qiita.com/advent-calendar/2020/wano-group)の13日目の記事になります。

Vue 3+Typescript (classなし) コンポネント間のデータとイベントの受け渡し方法を整理してメモしておきたいです。

## Vueネストコンポーネント

Vueのアプリケーションはネストされたコンポーネントのツリーの構成になっています。コンポーネントの階層の間データを受け渡す方法がいくつかあります。

### Props 
propsはプロパティの略、コンポーネントに登録できるカスタム属性です。Javascriptメソッドへ引数を渡すみたいにVueのコンポーネントへpropsを渡すことができます。

#### Propsの２つの特徴
- propsは下の階層のコンポーネントへ渡せる
- propsはread-only。更新したい場合は親のコンポーネントに新しい値を返して(emit event)親のコンポーネントで更新する必要がある

![chrome_RhgFMkOvI6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529952/842102da-2b17-9c4f-709d-00c71c01e78c.png)


### Propsを更新しないコンポネント

Propsを渡すのみone-wayの親と子コンポーネントの例です。

```vue:ParentComponent.vue

<template>
    <child-component
      :childStringProp="childString"
      :childObjectProp="childObject"
      :childObjectArrayProp="childObjectArray"
    />
</template>

<script lang="ts">
import { defineComponent } from "vue";
import ChildComponent from "@/components/ChildComponent.vue";
import ChildObject from "@/entity/ChildObject.vue";

export default defineComponent({
  components: {
    ChildComponent,
  },
  setup() {
    const childString = "string";
    const childObject: ChildObject = {
      test: "test"
    };
    const childObjectArray: ChildObject[] = [
      {
        test: "test",
      },
    ];
    return {
      // data
      childString,
      childObject,
      childObjectArray,
    };
  }
});
</script>
```
```vue:ChildComponent.vue

<template>
    {{ childString }}
    {{ childStringModified }}
    {{ childObject }}
    {{ childObjectArray }}
</template>

<script lang="ts">
import { defineComponent, computed, PropType} from "vue";
import ChildObject from "@/entity/ChildObject.vue";

export default defineComponent({
  props: {
    childString: String,
    childObject: ChildObject,
    childObjectArray: Array as PropType<ChildObject[]>,
  },
  setup(props) {
    // computed仕様の例も書いておきます
    const childStringModified = computed(() => {
      return props.childString + 'modified';
    });
    return {
      // computed
      childStringModified,
    };
  }
});
</script>
```
上の例の注意が必要なところは一つだけです。オブジェクトの配列を型を指定できないので、VueのPropTypeを使って型を指定できるようになります。

### Propsを更新するコンポネント

子のコンポーネントにデータの更新が必要なときにpropsの更新ではなく親のコンポネントへ更新イベントをemitする必要があります。

やり方が主に２つあります。
- 親のコンポーネントにVueのv-modelを仕様する
- 親にイベントを設定してfunctionを呼び出す

#### v-modelのtwo-way受け渡しの例

```vue:ParentComponent.vue

<template>
    <child-component
      v-model:childStringProp="childString"
      v-model:childString2Prop="childString2Computed"
    />
</template>

<script lang="ts">
import { defineComponent } from "vue";
import ChildComponent from "@/components/ChildComponent.vue";

export default defineComponent({
  components: {
    ChildComponent,
  },
  setup() {
    // refを使用したら変数がreactiveになり更新されたらUI上も表示が変わる
    const childString = ref<string>("string");
    const childString2 = ref<string>("string2");

    // getter&setterの使用の例、setterにemitして上のコンポーネントに返すこともできる
    const childString2Computed = computed({
      get: () => childObject.value,
      set: val => {
        childObject.value = val
      }
    });

    return {
      // data
      childString,
      childString2Computed,
    };
  }
});
</script>
```
```vue:ChildComponent.vue

<template>
    <input
    :value="childString"
    @input="$emit("update:childStringProp", e.target.value)"
  />
    <input
    :value="childString2"
    @input="childString2Update"
  />
</template>

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

export default defineComponent({
  emits: ["update:childStringProp", "update:childString2Prop"],
  props: {
    childString: String,
    childString2: String,
  },
  setup({emit}) {
    function childString2Update(e: any) {
      this.$emit("update:childString2Prop", e.target.value);
    }
    return {};
  }
});
</script>
```

注意するところはVue 3のref<>の使用することのとcomputedにgetter&setterの設定のことです。

#### 子コンポーネントのイベントを受け取る
も1つのtwo-wayデータ受け渡し方法はコンポーネント間のイベントです。

```vue:ParentComponent.vue

<template>
    <child-component
      childStringProp="childString"
      @updateChildStringProp="updateChildStringFunc"
    />
</template>

<script lang="ts">
import { defineComponent } from "vue";
import ChildComponent from "@/components/ChildComponent.vue";

export default defineComponent({
  components: {
    ChildComponent,
  },
  setup() {
    const childString = ref<string>("string");

    function updateChildStringFunc(val: string) {
      childString.value = val
    }
    return {
      // data
      childString,

      // func
      updateChildStringFunc,
    };
  }
});
</script>
```
```vue:ChildComponent.vue

<template>
    <input
    :value="childString"
    @input="childStringUpdate"
  />
</template>

<script lang="ts">
import { defineComponent, PropType} from "vue";
import ChildObject from "@/entity/ChildObject.vue";

export default defineComponent({
  emits: ["updateChildStringProp"],
  props: {
    childString: String,
  },
  setup() {
    function childStringUpdate(e: any) {
      this.$emit("updateChildStringProp", e.target.value);
    }
    return {};
  }
});
</script>
```

## まとめ
Vueのネスト構成は2-3階層の場合propsとemitは適切ですが、コンポーネントの階層が増えるたびに深いコンポネントまでたどり着くと可読性と保守性が落ちてきます。
また、ループの中のコンポーネントとIDに依存しているコンポーネントにもpropsの受け渡しは難しくなります。
繁雑なアプリにpropsではなくVuexやVue3のreactive storeなどのコンポーネント間にデータをシェアと管理できるツールを実装すればいいと思います。