🎐

【Vue3】CompositionAPIでpropsの変更を検知する

2021/06/20に公開

Vue3 Composition API でscript内でpropsの変更にを検知して特定のメソッドを呼び出す方法の解説です。

前提

親コンポーネントからVuexに保存されているstateを取得し、その値をpropsとして子コンポーネントに渡している場合に、子コンポーネントから特定のpropsの変更に応じてメソッドを実行したい場合の対処法です。
具体的なユースケースとしては、現在地の位置情報が変化した際にVuexのstateに保存し、stateの現在地をpropsで受け取りマップの中心を動かすメソッドを呼び出す場合などが挙げられます。

今回の例では分かりやすいように、メッセージを表示する例で紹介します。

src/store/index.ts
import { createStore } from "vuex";

export default createStore({
  state: {
    msg: "default msg",
  },
  mutations: {
    setMsg(state, msg) {
      state.msg = msg;
    },
  },
});
src/components/Parent.vue
<template>
  <Child :msg="msg" />
</template>

<script lang="ts">
import { defineComponent, computed } from "vue";
import { useStore } from "vuex";
import Child from "@/components/Child.vue";

export default defineComponent({
  name: "Home",
  components: {
    Child,
  },
  setup() {
    const store = useStore();
    const msg = computed(() => store.state.msg);
    
    // 0.5秒後に書き換え
    setTimeout(() => {
      store.commit("setMsg", "changeMsg");
    }, 500);
    
    // 1秒後に書き換え
    setTimeout(() => {
      store.commit("setMsg", "changeMsg2");
    }, 1000);

    return {
      msg,
    };
  },
});
</script>
src/components/Child.vue
<template>
  <div class="child">
    <h1>{{ msg }}</h1>
  </div>
</template>

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

export default defineComponent({
  name: "Child",
  props: {
    msg: String,
  },
  setup(props) {
    // ここでprops.msgが変更されたらメソッドを呼び出す
  },
});
</script>

結論

index.tsParent.vueは変更不要です。

index.ts
src/store/index.ts
import { createStore } from "vuex";

export default createStore({
  state: {
    msg: "default msg",
  },
  mutations: {
    setMsg(state, msg) {
      state.msg = msg;
    },
  },
});
Parent.vue
src/components/Parent.vue
<template>
  <Child :msg="msg" />
</template>

<script lang="ts">
import { defineComponent, computed } from "vue";
import { useStore } from "vuex";
import Child from "@/components/Child.vue";

export default defineComponent({
  name: "Home",
  components: {
    Child,
  },
  setup() {
    const store = useStore();
    const msg = computed(() => store.state.msg);
    
    // 0.5秒後に書き換え
    setTimeout(() => {
      store.commit("setMsg", "changeMsg");
    }, 500);
    
    // 1秒後に書き換え
    setTimeout(() => {
      store.commit("setMsg", "changeMsg2");
    }, 1000);

    return {
      msg,
    };
  },
});
</script>

Child.vueを次のように変更すれば変更を検知できます。

src/components/Child.vue
<template>
  <div class="child">
    <h1>{{ msg }}</h1>
  </div>
</template>

<script lang="ts">
- import { defineComponent } from "vue";
+ import { defineComponent, toRefs, watch } from "vue";

export default defineComponent({
  name: "Child",
  props: {
    msg: String,
  },
  setup(props) {
+   const { msg } = toRefs(props);
+   watch(msg, () => {
+     // ここで任意のメソッドを呼び出す
+   });
  },
});
</script>

解説

NGパターン

真っ先に思いつきそうな方法として、watchの引数にprops.msgと指定する方法はうまくいきません。
watchの引数はリアクティブなオブジェクトである必要があるため、props.msg(今回はstring)は指定できません。

ng.vue
watch(props.msg, () => {
  // ここで任意のメソッドを呼び出す
});

ですが、リアクティブなオブジェクトであるpropswatchに指定すると、propsのいずれかの値が変化した場合に毎回呼び出されてしまいます。

ng.vue
watch(props, () => {
  // ここで任意のメソッドを呼び出す
});

toRefsで値をリアクティブで取り出す

解決策としては、特定の値をリアクティブで取り出すために、toRefsメソッドを使います。
toRefsメソッドはオブジェクトの特定のプロパティの参照を作成する関数です。
参考:https://v3.ja.vuejs.org/api/refs-api.html#toref

toRefsを使って特定のプロパティの参照オブジェクトを作成することで、watchの引数に指定できるようになります。

src/components/Child.vue
<template>
  <div class="child">
    <h1>{{ msg }}</h1>
  </div>
</template>

<script lang="ts">
- import { defineComponent } from "vue";
+ import { defineComponent, toRefs, watch } from "vue";

export default defineComponent({
  name: "Child",
  props: {
    msg: String,
  },
  setup(props) {
+   const { msg } = toRefs(props);
+   watch(msg, () => {
+     // ここで任意のメソッドを呼び出す
+   });
  },
});
</script>

Vue3 Composition API でscript内でpropsの変更にを検知して特定のメソッドを呼び出す方法でした。

Discussion