Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to Watch Props Change with Vue Composition API / Vue 3?

While Vue Composition API RFC Reference site has many advanced use scenarios with the watch module, there is no examples on how to watch component props?

Neither is it mentioned in Vue Composition API RFC's main page or vuejs/composition-api in Github.

I've created a Codesandbox to elaborate this issue.

<template>
  <div id="app">
    <img width="25%" src="./assets/logo.png">
    <br>
    <p>Prop watch demo with select input using v-model:</p>
    <PropWatchDemo :selected="testValue"/>
  </div>
</template>

<script>
import { createComponent, onMounted, ref } from "@vue/composition-api";
import PropWatchDemo from "./components/PropWatchDemo.vue";

export default createComponent({
  name: "App",
  components: {
    PropWatchDemo
  },
  setup: (props, context) => {
    const testValue = ref("initial");

    onMounted(() => {
      setTimeout(() => {
        console.log("Changing input prop value after 3s delay");
        testValue.value = "changed";
        // This value change does not trigger watchers?
      }, 3000);
    });

    return {
      testValue
    };
  }
});
</script>
<template>
  <select v-model="selected">
    <option value="null">null value</option>
    <option value>Empty value</option>
  </select>
</template>

<script>
import { createComponent, watch } from "@vue/composition-api";

export default createComponent({
  name: "MyInput",
  props: {
    selected: {
      type: [String, Number],
      required: true
    }
  },
  setup(props) {
    console.log("Setup props:", props);

    watch((first, second) => {
      console.log("Watch function called with args:", first, second);
      // First arg function registerCleanup, second is undefined
    });

    // watch(props, (first, second) => {
    //   console.log("Watch props function called with args:", first, second);
    //   // Logs error:
    //   // Failed watching path: "[object Object]" Watcher only accepts simple
    //   // dot-delimited paths. For full control, use a function instead.
    // })

    watch(props.selected, (first, second) => {
      console.log(
        "Watch props.selected function called with args:",
        first,
        second
      );
      // Both props are undefined so its just a bare callback func to be run
    });

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

EDIT: Although my question and code example was initially with JavaScript, I'm actually using TypeScript. Tony Tom's first answer although working, lead to a type error. Which was solved by Michal Levý's answer. So I've tagged this question with typescript afterwards.

EDIT2: Here is my polished yet barebones version of the reactive wirings for this custom select component, on top of <b-form-select> from bootstrap-vue (otherwise agnostic-implementation but this underlying component does emit @input and @change events both, based on whether change was made programmatically or by user interaction).

<template>
  <b-form-select
    v-model="selected"
    :options="{}"
    @input="handleSelection('input', $event)"
    @change="handleSelection('change', $event)"
  />
</template>

<script lang="ts">
import {
  createComponent, SetupContext, Ref, ref, watch, computed,
} from '@vue/composition-api';

interface Props {
  value?: string | number | boolean;
}

export default createComponent({
  name: 'CustomSelect',
  props: {
    value: {
      type: [String, Number, Boolean],
      required: false, // Accepts null and undefined as well
    },
  },
  setup(props: Props, context: SetupContext) {
    // Create a Ref from prop, as two-way binding is allowed only with sync -modifier,
    // with passing prop in parent and explicitly emitting update event on child:
    // Ref: https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier
    // Ref: https://medium.com/@jithilmt/vue-js-2-two-way-data-binding-in-parent-and-child-components-1cd271c501ba
    const selected: Ref<Props['value']> = ref(props.value);

    const handleSelection = function emitUpdate(type: 'input' | 'change', value: Props['value']) {
      // For sync -modifier where 'value' is the prop name
      context.emit('update:value', value);
      // For @input and/or @change event propagation
      // @input emitted by the select component when value changed <programmatically>
      // @change AND @input both emitted on <user interaction>
      context.emit(type, value);
    };

    // Watch prop value change and assign to value 'selected' Ref
    watch(() => props.value, (newValue: Props['value']) => {
      selected.value = newValue;
    });

    return {
      selected,
      handleSelection,
    };
  },
});
</script>
like image 694
ux.engineer Avatar asked Dec 01 '19 13:12

ux.engineer


People also ask

How do you use props in composition API Vue?

How to access the props object inside <script setup> ? In the composition API, you need to explicitly define the props in <script setup> to be able to use the props object.

Can you watch a prop Vuejs?

To listen for props changes with Vue. js, we can add a watcher for the prop. to watch the value of the myProp prop by setting watch. myProp to an object with immediate set to true to watch the initial value of the myProp prop.

Are Props reactive in Vue 3?

Props gotten directly from vue 3 setup are not reactive.


4 Answers

If you take a look at watch typing here it's clear the first argument of watch can be array, function or Ref<T>

props passed to setup function is reactive object (made probably by readonly(reactive()), it's properties are getters. So what you doing is passing the value of the getter as the 1st argument of watch - string "initial" in this case. Because Vue 2 $watch API is used under the hood (and same function exists in Vue 3), you are effectively trying to watch non-existent property with name "initial" on your component instance.

Your callback is called only once and never again. Reason it is called at least once is because new watch API is behaving like current $watch with immediate option (UPDATE 03/03/2021 - this was later changed and in release version of Vue 3, watch is lazy same way as it was in Vue 2)

So by accident you doing the same thing Tony Tom suggested but with wrong value. In both cases it's not valid code if you are using TypeScript

You can do this instead:

watch(() => props.selected, (first, second) => {
      console.log(
        "Watch props.selected function called with args:",
        first,
        second
      );
    });

Here the 1st function is executed immediately by Vue to collect dependencies (to know what should trigger the callback) and 2nd function is the callback itself.

Other way would be to convert props object using toRefs so it's properties would be of type Ref<T> and you can pass them as a 1st argument of watch

Anyway, most of the time watching props is just not needed - simply use props.xxx directly in your template (or setup) and let the Vue do the rest

like image 141
Michal Levý Avatar answered Oct 21 '22 11:10

Michal Levý


I just wanted to add some more details to the answer above. As Michal mentioned, the props coming is an object and is reactive as a whole. But, each key in the props object is not reactive on its own.

We need to adjust the watch signature for a value in the reactive object compared to a ref value

// watching value of a reactive object (watching a getter)

watch(() => props.selected, (selection, prevSelection) => { 
   /* ... */ 
})
// directly watching a ref

const selected = ref(props.selected)

watch(selected, (selection, prevSelection) => { 
   /* ... */ 
})

Just some more info even though it's not the mentioned case in the question: If we want to watch on multiple properties, one can pass an array instead of a single reference

// Watching Multiple Sources

watch([ref1, ref2, ...], ([refVal1, refVal2, ...],[prevRef1, prevRef2, ...]) => { 
   /* ... */ 
})

like image 24
Syam Pillai Avatar answered Oct 21 '22 11:10

Syam Pillai


This does not address the question of how to "watch" properties. But if you want to know how to make props responsive with Vue's Composition API, then read on. In most cases you shouldn't have to write a bunch of code to "watch" things (unless you're creating side effects after changes).

The secret is this: Component props IS reactive. As soon as you access a particular prop, it is NOT reactive. This process of dividing out or accessing a part of an object is referred to as "destructuring". In the new Composition API you need to get used to thinking about this all the time--it's a key part of the decision to use reactive() vs ref().

So what I'm suggesting (code below) is that you take the property you need and make it a ref if you want to preserve reactivity:

export default defineComponent({
  name: 'MyAwesomestComponent',
  props: {
    title: {
      type: String,
      required: true,
    },
    todos: {
      type: Array as PropType<Todo[]>,
      default: () => [],
    },
    ...
  },
  setup(props){ // this is important--pass the root props object in!!!
    ...
    // Now I need a reactive reference to my "todos" array...
    var todoRef = toRefs(props).todos
    ...
    // I can pass todoRef anywhere, with reactivity intact--changes from parents will flow automatically.
    // To access the "raw" value again:
    todoRef.value
    // Soon we'll have "unref" or "toRaw" or some official way to unwrap a ref object
    // But for now you can just access the magical ".value" attribute
  }
}

I sure hope the Vue wizards can figure out how to make this easier... but as far as I know this is the type of code we'll have to write with the Composition API.

Here is a link to the official documentation, where they caution you directly against destructuring props.

like image 27
Robert Avatar answered Oct 21 '22 10:10

Robert


In my case I solved it using key

<MessageEdit :key="message" :message="message" />

Maybe on your case would look something like this

<PropWatchDemo :key="testValue" :selected="testValue"/>

But I don't have any idea of its pros and cons versus watch

like image 1
renzivan Avatar answered Oct 21 '22 11:10

renzivan