145

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>
tony19
  • 125,647
  • 18
  • 229
  • 307
ux.engineer
  • 10,082
  • 13
  • 67
  • 112
  • 1
    Why can't you just use the `watch` on the props which you take into the `setup` function? First make them into `Refs, basiacally make a reactive copy and it should fire on subsequent changes. – Michael Dec 01 '19 at 13:36
  • 2
    This isn't the right question. We shouldn't have to watch props in Vue! The fact that you can't destructure props like we could in Vue 2 seems like a big step backwards. See "toRefs" (and in the future "toRef") to see how you can avoid this anti-pattern of watching a prop just to set another value. – Robert May 02 '20 at 15:20
  • I've added an alternative answer if you would rather keep the props reactive vs have to write "watch" code all the time. – Robert May 02 '20 at 16:01

8 Answers8

193

If you take a look at watch typings here it makes it clear the first argument of watch could be an array, function or Ref<T>

props passed to the setup function are a reactive object (likely by readonly(reactive()), it's properties are getters. So what you are 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 was only called once. The reason it was called at least once is because the new watch API is behaving like the current $watch with the 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 are doing the same thing Tony Tom suggested but with the wrong value. In both cases, it is not valid code when 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 the first argument of watch.

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

Zymotik
  • 6,412
  • 3
  • 39
  • 48
Michal Levý
  • 33,064
  • 4
  • 68
  • 86
  • I've renamed the first parameter as 'newValue'. Having it typed with 'any' works but with specific types am getting a type error: Argument of type '() => string | number | boolean | undefined' is not assignable to parameter of type 'WatcherSource[]'. Type '() => string | number | boolean | undefined' is missing the following properties from type 'WatcherSource[]': pop, push, concat, join, and 25 more. – ux.engineer Dec 01 '19 at 16:58
  • However, using indexed access type my props type interface is not giving an error: Props['selected'] – ux.engineer Dec 01 '19 at 17:15
  • Sorry, I'm not a TypeScript expert (yet). What you want is use 2nd definition from typings referenced in my answer (`function watch`). Maybe share sandbox with TS solution ? btw do you really need old value in the callback ? – Michal Levý Dec 01 '19 at 17:38
  • Actually the second parameter was undefined so not sure how I could access the old value...but that was not needed in this case. I've added an edit to my question with a more polished example for all these reactive wirings. – ux.engineer Dec 01 '19 at 18:06
  • First argument can be a getter/effect function, a ref, a reactive object, or an array of these types. – Dmitry Chernikov Jan 14 '23 at 10:51
72

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) => { 
   /* ... */ 
})

Note: See comment from Michal Levý below before using this potentially erroneous code:

// directly watching a value

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, ...]) => { 
   /* ... */ 
})

Zymotik
  • 6,412
  • 3
  • 39
  • 48
Syam Pillai
  • 4,967
  • 2
  • 26
  • 42
  • one more example: watching ref and reactive (including props) together: ```watch([ref, () => reactiveObj.property, () => prop.val], ([refNew, reactiveNew, propValNew], [refOld, reactiveOld, propValOld]) => { /* Code here */ });``` – Ashwin Bande Nov 07 '20 at 12:04
  • 4
    I have sort of "deja vu" feeling here as I think I already wrote this comment before **but** the second example is **very dangerous and wrong** as it creates new `selected` ref and **initializes** it with current value of `props.selected`. Problem is from that point that `selected` ref is completely disconnected from the `props.selected` value (if prop value changes, ref will not). This is in most cases NOT what you want. – Michal Levý Jul 06 '22 at 13:14
30

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.

Robert
  • 1,220
  • 16
  • 19
3

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

renzivan
  • 121
  • 5
  • 2
    a changing `key` make vue re-render a component, so yes, the value of the prop is reloaded but it not the best solution here as if you don't want the component to be re-rendered it should be able to watch the props change directly to handle them inside. Best usage of `key` is when using `transition` or `transition-group` – Stephen Milic Feb 24 '23 at 00:12
1

Change your watch method like below.

 watch("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
    });
Tony Tom
  • 1,435
  • 1
  • 10
  • 17
  • Nice catch! An additional error however, as am using TypeScript - ideas on this? My props argument on setup function does have a custom interface type: Argument of type '"selected"' is not assignable to parameter of type 'WatcherSource[]'. – ux.engineer Dec 01 '19 at 14:54
  • 4
    This doesn't make sense. Watch source can't be a String. – m4heshd Apr 02 '21 at 14:41
  • @m4heshd Well in Vue 2 the `watch` argument could be a string (at least in Options API) - in that case the watcher was on the Vue instance (component) property with the same name as the string passed. All the confusion comes from the fact that 1st versions of composition API was build as a plugin on top of Vue 2 options API – Michal Levý Jan 11 '22 at 18:41
  • @MichalLevý I did know that. But isn't the question regarding Vue 3? – m4heshd Jan 11 '22 at 19:55
  • 1
    @m4heshd Well yes it is now, but when I (and Tony) originally answered it was about Vue2 + Composition API plugin (just watch the history of edits). You can still see a remnants of that in my answer...need to update it I guess – Michal Levý Jan 11 '22 at 20:17
  • @MichalLevý Ohhh I see. :) – m4heshd Jan 13 '22 at 03:51
0

please note that props cannot be updated directly inside the child component, so no updates, no watch triggers!

if you want to have the updated values, there are several approaches:

  1. use getter setter computed for props that you want to update and then emit them to parent component.
  2. instead of props, use provide/inject ( I know this is when you want to share data in a big component tree! but it comes handy when you have a big form data that needs to be reactive! )
0

Try this one - works for me:

const currentList = computed(() => props.items)
watch(currentList, (newValue, oldValue) => {
  console.log(newValue,oldValue)
})
  • 1
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jul 16 '23 at 09:00
-2

None of the options above worked for me but I think I found a simple way that seems to works very well to keep vue2 coding style in composition api

Simply create a ref alias to the prop like:

myPropAlias = ref(props.myProp)

and you do everything from the alias

works like a charm for me and minimal

Sam
  • 1,557
  • 3
  • 26
  • 51
  • 7
    To create an alias you need to use `toRefs` or `toRef`. What you creating is not an alias. It is new `ref` initialized with the **current value** of prop. When parent updates the prop to a new value, your "alias" will not change. This is in most cases not what you want – Michal Levý Jan 12 '22 at 10:25