Vue Composition APIではどうやるの?と思った事とその方法

Vue 3の新しいAPIであるComposition APIを使用して、調べたこと考察したことをまとめています。気付きがあれば追記します。

Web

変更履歴

前提

Vue 3の新しいAPIであるComposition APIを使用して、調べたこと考察したことをまとめています。 Vueの既存のAPIであるOptions APIやProperty Decorator(TypeScript)の使用経験はあり、その上でComposition APIではどのように実現するのか調べた点を中心に記載します。

Composition APIの使用環境はVue 2とComposition APIプラグインを使用しています。 Vue 3のリリース版とは仕様が異なる可能性があります。

使用バージョン

  • Vue 2.x
  • @vue/composition-api v0.5.0 以降

プロパティの監視(watch)

watch関数でプロパティを監視

watch関数でプロパティの値を監視する場合は、最初の引数にプロパティの値を返す関数を指定します。

JavaScript
import { watch } from '@vue/composition-api';

export default {
  props: ['value'],
  setup(props) {
    watch(() => props.value, (newVal, oldVal) => {
      console.log(`${oldVal} -> ${newVal}`);
    });
  },
};

watchEffect関数でプロパティを監視

watchEffect関数でプロパティの値を監視する場合は、コールバック関数内でプロパティを参照することで自動的に監視されます。

JavaScript
import { watchEffect } from '@vue/composition-api';

export default {
  props: ['value'],
  setup(props) {
    watchEffect(() => {
      console.log(props.value);
    });
  },
};

リファレンスIDを指定した子コンポーネントへのアクセス

リファレンスIDを指定した子コンポーネントへのアクセス(単体)

setup関数でリファレンスIDと同じ名前のRefを返すことで、マウント後にコンポーネント/Elementが代入されます。

Template
<div ref="elementRef"></div>
JavaScript
import { ref, onMounted } from '@vue/composition-api';

export default {
  setup() {
    const elementRef = ref();
    onMounted(() => {
      console.log(elementRef.value);
    });
    return {
      elementRef,
    };
  },
};

リファレンスIDを指定した子コンポーネントへのアクセス(可変数)

v-for ディレクティブで生成した複数の要素を参照したい場合は、setup関数でリファレンスIDと同じ名前で、要素を受け取る配列のRefを返します。 元のデータ配列が変化して要素が増減した場合は、この配列にもリアクティブに反映されます。

Template
<ul>
  <li v-for="item in items" :key="item" ref="elementsRef">
    {{ item }}
  </li>
</ul>
JavaScript
import { ref, watchEffect } from '@vue/composition-api';

export default {
  setup() {
    const elementsRef = ref([]);
    const items = [];
    watchEffect(() => console.log([...elementsRef.value]));
    setInterval(() => items.push(Math.random()), 1000);
    return {
      elementsRef,
      items,
    };
  },
};

リファレンスIDを指定した子コンポーネントへの型付アクセス(TypeScript)

Refを使用して子コンポーネントにアクセスする方法は前記の通りです。 TypeScriptでコンポーネントのRefに型を紐づけるには ref<InstanceType<typeof Component>>() のように変数を初期化します。

例として、MyCounter コンポーネントが count 変数と increment 関数を持つ場合、

MyCounter.vue(TypeScript)
import { defineComponent, ref } from '@vue/composition-api';

export default defineComponent({
  setup() {
    const count = ref(0);
    function increment() {
      count.value += 1;
    }
    return {
      count,
      increment,
    };
  },
});

親コンポーネントから次のようにアクセスできます。

Template
<MyCounter ref="myCounter" />
TypeScript
import { defineComponent, ref, watchEffect } from '@vue/composition-api';
import MyCounter from './MyCounter.vue';

export default defineComponent({
  components: {
    MyCounter,
  },
  setup() {
    const myCounter = ref<InstanceType<typeof MyCounter>>();
    watchEffect(() => console.log(myCounter.value?.count));
    setInterval(() => {
      if (myCounter.value) {
        myCounter.value.increment();
      }
    }, 1000);
    return {
      myCounter,
    };
  },
});

コンポーネントのインスタンスにアクセス

getCurrentInstance関数でコンポーネントのインスタンスを取得できます。

JavaScript
import { getCurrentInstance } from '@vue/composition-api';

export default {
  setup() {
    console.log(getCurrentInstance());
  },
};

基本的には、直接インスタンスにアクセスしなくてもComposition APIで操作できるはずなので乱用は避けた方がいいと思います。

Composition APIに未対応のプラグイン(インスタンス)にアクセス

次のコードは、i18nを利用する例です。 この例では1ファイルにまとめていますが、useTranslation関数はモジュール化してコンポーネント間で共有するといいと思います。

JavaScript
import { getCurrentInstance } from '@vue/composition-api';

function useTranslation() {
  const vm = getCurrentInstance();
  return {
    i18n: vm.$i18n,
    t: (...args) => vm.$t(...args),
  };
}

export default {
  setup() {
    const { t, i18n } = useTranslation();
    console.log(i18n.locale);
    console.log(t('foo'));
  },
};

プロパティに具体的な型を指定(TypeScript)

TypeScriptでプロパティに具体的な型を指定するにはdefineComponentとPropTypeを使用します。 これでsetup関数のprops引数に型が反映されて、エディタのコード補完や型チェックの恩恵を受けられます。

TypeScript
import { defineComponent, PropType } from '@vue/composition-api';

interface Item {
  x: number;
  y: number;
}

export default defineComponent({
  props: {
    item: Object as PropType<Item>,
    allItems: {
      type: Array as PropType<Item[]>,
      default: () => [],
    },
  },
  setup(props) {
    // props に指定した型が反映されている
  },
});

リアクティブなオブジェクトのプロパティの値を関数で受け取る

値のままプロパティを関数で受け取る

プロパティの値に対してリアクティブでなくてよければ、普通に値でプロパティを受け取ります。 言い換えると、リアクティブな動作が求められる場合は、以降で説明する方法で受け取る必要があります。

JavaScript
function useExample(value) {
  console.log(value);
}

export default {
  props: ['value'],
  setup(props) {
    useExample(props.value);
  },
};

リアクティブなオブジェクトごとプロパティを関数で受け取る

リアクティブなオブジェクトごとプロパティを受け取ることで、関数内でプロパティを参照してリアクティブな動作を実現できます。 ただし、この方法には後述の考慮すべき欠点もあります。

JavaScript
import { watchEffect } from '@vue/composition-api';

function useExample(props) {
  watchEffect(() => {
    console.log(props.value);
  });
}

export default {
  props: ['value'],
  setup(props) {
    useExample(props);
  },
};

TypeScriptでプロパティのオブジェクトを持ちまわる場合は、次のように型を指定します。

TypeScript
import { defineComponent, watchEffect } from '@vue/composition-api';

interface Props {
  value: number;
}

function useExample(props: Props) {
  watchEffect(() => {
    console.log(props.value);
  });
}

export default defineComponent({
  props: {
    value: Number,
  },
  setup(props: Props) {
    useExample(props);
  },
});

※ここからは、個人の考察が入ります。

関数でプロパティのオブジェクトを丸ごと受け取ることについては、次の欠点があると思います。

  • どの関数がどのプロパティを参照しているか、関数の実装を見るまでコードからは把握できない
  • 関数に特定の形式でプロパティのオブジェクトを受け渡すことを要求されるため、再利用しにくく(取り回しが悪く)なる可能性がある

これらの欠点は、次のリンクで語られているComposition APIを使用するメリットである「機能ごとの依存関係を一覧性のある形でコードに表現できる」こと「ロジックを抽出して再利用できる」ことを多少なりとも打ち消すものだと思います。

https://composition-api.vuejs.org/#code-organization

では、この方法が必ずしも悪いかというと、そういうわけではなく、それは開発するコンポーネントの規模や開発方針によると思います。 例えば、小規模なコンポーネントで内部的に使う関数の場合、実害が無ければ、細かくプロパティを引き渡すよりも開発効率が良いという判断もあると思います。

もしそうではなく、個別のプロパティを明示的に関数に受け渡す方が良いと判断する場合、以降で説明する方法が使えます。

プロパティの値を返す関数(Getter)を関数で受け取る

プロパティの値を返す関数(Getter)を受け取ることで、関数内で個別のプロパティをリアクティブに参照できます。 そして、この関数がプロパティの値を書き換えないことが明確になります。

この方法の欠点は、引数の型が一見して分かりにくい点だと思います。 対処としては、変数の命名規則による型の示唆やTypeScriptによる型の明示を検討すると良いかもしれません。

JavaScript
import { watch, computed } from '@vue/composition-api';

function useFoo(fooGetter) {
  watch(fooGetter, (newVal, oldVal) => {
    console.log(`${oldVal} -> ${newVal}`);
  });
}

function useBar(barGetter) {
  return {
    doubleBar: computed(() => barGetter() * 2),
  };
}

export default {
  props: {
    foo: Number,
    bar: { type: Number, default: 0 },
  },
  setup(props) {
    useFoo(() => props.foo);
    return useBar(() => props.bar);
  },
};
TypeScript
import { defineComponent, watch, computed } from '@vue/composition-api';

function useFoo(foo: () => number|undefined) {
  watch(foo, (newVal, oldVal) => {
    console.log(`${oldVal} -> ${newVal}`);
  });
}

function useBar(bar: () => number) {
  return {
    doubleBar: computed(() => bar() * 2),
  };
}

export default defineComponent({
  props: {
    foo: Number,
    bar: { type: Number, default: 0 },
  },
  setup(props) {
    useFoo(() => props.foo);
    return useBar(() => props.bar);
  },
});

プロパティに連動したRefを関数で受け取る

プロパティに連動したRefを受け取ることで、関数内で個別のプロパティをリアクティブに参照および変更できます。

関数を使用する側は、computed関数などを使用してリアクティブなプロパティと連動したRefを生成して関数に渡します。 もしくは、最初からRefの値であればそのまま渡します。

JavaScript
import { watch, computed } from '@vue/composition-api';

function useFoo(fooRef) {
  watch(fooRef, (newVal, oldVal) => {
    console.log(`${oldVal} -> ${newVal}`);
    if (newVal > 10) {
      fooRef.value = 0;
    }
  });
}

function useBar(barRef) {
  return {
    doubleBar: computed(() => barRef.value * 2),
  };
}

export default {
  props: {
    foo: { type: Number, default: 0 },
    bar: { type: Number, default: 0 },
  },
  setup(props, { emit }) {
    const fooRef = computed({
      get: () => props.foo,
      set: (val) => emit('update:foo', val),
    });
    useFoo(fooRef);
    return useBar(computed(() => props.bar));
  },
};
TypeScript
import { defineComponent, watch, computed, Ref } from '@vue/composition-api';

function useFoo(foo: Ref<number>) {
  watch(foo, (newVal, oldVal) => {
    console.log(`${oldVal} -> ${newVal}`);
    if (newVal > 10) {
      foo.value = 0;
    }
  });
}

function useBar(bar: Readonly<Ref<number>>) {
  return {
    doubleBar: computed(() => bar.value * 2),
  };
}

export default defineComponent({
  props: {
    foo: { type: Number, default: 0 },
    bar: { type: Number, default: 0 },
  },
  setup(props, { emit }) {
    const foo = computed({
      get: () => props.foo,
      set: (val) => emit('update:foo', val),
    });
    useFoo(foo);
    return useBar(computed(() => props.bar));
  },
});