Vue.jsのwatchが動かない謎を解け


問題

早速ですが以下のコードを見てください。
カウントアップボタンを1回クリックした際に、最初に出力される数字は何でしょう?
出力している部分はcomponents/Counter.vueの①の部分です。

(A) 0
(B) 1
(C) 2
(D) 何も出ない

pages/index.vue
<template>
  <div>
    <counter v-if="counter > 0" />
    <button type="button" @click="increment">カウントアップ</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, inject, provide } from "@nuxtjs/composition-api";
import useCounterStore, { CounterStore, CounterStoreKey } from "~/stores/CounterStore";
import Counter from "~/components/Counter.vue";

export default defineComponent({
  components: { Counter },
  setup() {
    provide(CounterStoreKey, useCounterStore());

    const { counter } = inject(CounterStoreKey) as CounterStore;

        const increment = () => counter.value++;

    return {
      counter,
      increment
    };
  },
});
</script>
components/Counter.vue
<template>
  <div>{{ counter }}</div>
</template>

<script lang="ts">
import { defineComponent, inject, watch } from "@nuxtjs/composition-api";
import { CounterStore, CounterStoreKey } from "~/stores/CounterStore";

export default defineComponent({
  setup() {
    const { counter } = inject(CounterStoreKey) as CounterStore;

    watch(counter, () => console.log(counter.value)); // ①

    return {
      counter,
    };
  },
});
</script>
stores/CounterStore.ts
import { InjectionKey, ref } from "@nuxtjs/composition-api"

const useCounterStore = () => {
  const counter = ref(0)
  return { counter }
}

export type CounterStore = ReturnType<typeof useCounterStore>
export const CounterStoreKey: InjectionKey<CounterStore> = Symbol('CounterStore')
export default useCounterStore

正解

(D)の「何も出ない」です。

理由

この問題のカギとなるのは、v-ifです。
pages/index.vueで、components/Counter.vueの表示に、v-ifを利用しています。
Vue.jsを使ったことがある人だったらわかると思いますが、v-ifは特定の条件の場合のみ表示するためのディレクティブです。

今回の場合、counter > 0の場合に表示されるようになっているので、初期表示時はcounter = 0なので表示されないようになっています。さらに、v-ifはHTMLタグ自体も出力しないため、Counterコンポーネントがページ上にない状態です。つまり、watchも動作しないため、0 → 1の変更の検知はできないということになります。

最初のカウントアップで、counterが1となり、Counterコンポーネント が表示されwatchが設定されるようになり、以降、カウンターの変更を検知しwatchで指定した関数が実行されます。

どうすればよいか

v-if ではなく、v-showを使うことで、問題を解決できます。v-showはHTML自体は出力され、CSSのdisplayで表示切り替えを行います。Counterコンポーネント自体は出力されているので、画面上見えてなくても、watchは動くようになります。