Discover the magic of observing prop changes in Vue Composition API / Vue 3!

Exploring the Vue Composition API RFC Reference site, it's easy to find various uses of the watch module, but there is a lack of examples on how to watch component props.

This crucial information is not highlighted on the main page of Vue Composition API RFC or even on Github's vuejs/composition-api.

To address this issue, I have created a detailed demonstration in a Codesandbox.

<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 it's just a bare callback func to be run
    });

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

My initial question and code example were based on JavaScript, but now I'm using TypeScript. The first answer by Tony Tom worked, but there was a type error which I managed to resolve with Michal Levý's answer. Therefore, I added the typescript tag to this question.

Here is an enhanced version of the reactive connection for a custom select component, built on top of <b-form-select> from bootstrap-vue. Although the implementation is agnostic, this underlying component emits @input and @change events depending on user interaction or programmatic changes.

<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>

Answer №1

When examining the typings for the watch function here, it becomes apparent that the first argument of watch can be an array, function, or Ref<T>

The props passed to the setup function are a reactive object (most likely through readonly(reactive())). Its properties act as getters. Therefore, by passing the value of the getter as the 1st argument of watch (in this case being the string "initial"), you end up trying to watch a non-existent property named "initial" on your component instance since Vue 2's $watch API is utilized under the hood (with the same function also existing in Vue 3).

Your callback is only triggered once, and it was called at least once due to the new watch API initially behaving like the current $watch with the immediate option (UPDATE 03/03/2021 - this behavior has since been changed and in the release version of Vue 3, watch is lazy just as it was in Vue 2).

Therefore, unintentionally, you are essentially following what Tony Tom recommended but with the incorrect value. In both scenarios, when using TypeScript, the code is invalid.

A better approach would be:

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

In this example, the 1st function is immediately executed by Vue to gather dependencies (to determine what should trigger the callback), while the 2nd function serves as the actual callback.

Alternatively, you could convert the props object using toRefs so that its properties become of type Ref<T>, allowing you to pass them as the first argument of watch.

However, most of the time, watching props is unnecessary. Simply utilize props.xxx directly in your template (or setup) and let Vue handle the rest.

Answer №2

Expanding on the previous answer, it's important to note that while the props object is reactive as a whole in Vue.js, each key within the props object is not individually reactive.

When working with values within a reactive object, the watch signature needs to be adjusted compared to when dealing with a single ref value.

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

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

Note: Please refer to Michal Levý's comment below before using the code above as there may be potential errors:

// Directly watching a value

const selected = ref(someValue)

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

In addition, if you need to watch multiple properties simultaneously, you can pass an array containing the references instead of a single reference:

// Watching Multiple Sources

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

Answer №3

When it comes to making props responsive using Vue's Composition API, there is a key concept to understand that involves accessing and preserving reactivity. While the traditional method of "watching" properties may not always be necessary, utilizing `ref` can help maintain reactivity in your components.

In the Composition API, it is important to recognize that component `props` are reactive by nature. However, once you access a specific prop, it loses its reactivity. This process of breaking down or accessing a part of an object is known as "destructuring". Therefore, when working with props in the new Composition API, consider converting the required property into a `ref` to ensure reactivity is preserved:

export default defineComponent({
  name: 'MyAwesomestComponent',
  props: {
    title: {
      type: String,
      required: true,
    },
    todos: {
      type: Array as PropType<Todo[]>,
      default: () => [],
    },
    ...
  },
  setup(props){ // make sure to pass the root props object here!!!
    ...
    // Creating a reactive reference for the "todos" array...
    var todoRef = toRefs(props).todos
    ...
    // By passing todoRef, reactivity will be maintained automatically.
    // To retrieve the original value:
    todoRef.value
    // In the future, there might be tools like "unref" or "toRaw" to unwrap a ref object officially.
    // For now, simply use the ".value" attribute to access the underlying value.
  }
}

While the process may seem complex, mastering this approach is crucial for effective use of the Composition API within Vue. Stay tuned for updates from the Vue development team on simplifying these procedures. Meanwhile, refer to the official documentation for further guidance, which advises against excessive destructuring of props.

Answer №4

My solution involved utilizing the key

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

Perhaps for your situation, it could resemble something like this

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

However, I am unsure of the advantages and disadvantages compared to using watch

Answer №5

Modify the watch method as shown in the example below.

 watch("selected", (initial, final) => {
      console.log(
        "The watch props.selected function has been invoked with arguments:",
        initial, final
      );
      // Since both properties are undefined, this is just a simple callback function to execute
    });

Answer №6

Keep in mind that props cannot be directly changed within the child component, so no updates will trigger any watches!

If you desire to access updated values, there are multiple methods:

  1. Utilize getter setter computed properties for props you wish to modify and then emit them to the parent component.
  2. Instead of props, consider using provide/inject (typically used for sharing data within a complex component tree, but can also serve well for reactive form data!)

Answer №7

Give this a shot! It's been effective for me:

const myList = computed(() => props.items)
watch(myList, (newVal, oldVal) => {
  console.log(newVal,oldVal)
})

Answer №8

observe string, number, and boolean properties using arrow functions:

watch(() => props.primitiveType, (val, oldVal) => {})      

track changes in object and array properties with deep observation using arrow functions:

watch(() => props.complexType, (val, oldVal) => {}, {deep:true})

alternatively,

watch(props.complexType, (val, oldVal) => {}) // this works too

checkout more details on vue docs

Answer №9

If none of the options provided above are working for you, here's a simple solution I discovered that effectively maintains the vue2 coding style within the composition API.

All you need to do is create an alias using ref for the prop, like this:

myPropAlias = ref(props.myProp)

Then, you can perform all operations using this alias.

This method has been working flawlessly for me and is extremely efficient.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Extract CSS from Chrome developer tools and convert it into a JavaScript object

Recently, we have started incorporating styles into our React components using the makeStyles hook from Material-UI. Instead of traditional CSS, we are now using JavaScript objects to define styles. An example of this is shown below: const useStyles = ma ...

Guide to developing interactive elements in VueJS

I recently started working with vueJS and have been attempting to dynamically load components. Despite trying various suggestions found online, I still haven't been successful. Here is the scenario: I want to create a 'shell' component that ...

Unable to send post parameters to Yii2 controller using an XHR request

Within the context of my project, I am making an xhr request to a yii2 controller. Here is how the request is structured in my view: var xhr = new XMLHttpRequest(); xhr.open('POST', '$urlToController', true); xhr.setRequestHeader("Co ...

The length of Array(n) is incorrect when running on production in Next.js

My goal is to create an array of a specific length in TypeScript, but I am encountering an issue where the array length is incorrect when running my app in production mode (minified version). For example, when I execute console.log(Array(3)); in developme ...

Waiting for Cypress to finish loading the table

After typing text into the input component, I am attempting to test and click on the table row that appears. The table is loaded with a delay after receiving a response from an external API. What has worked for me so far is using a timeout that waits until ...

AngularJS continues to display an "Invalid Request" message whenever the requested resource is unavailable

When I try to fetch a group of images through AJAX and exhibit them on the page using ng-repeat, an issue arises with the image source tags. These tags send incorrect requests to the server before the collection is retrieved, leading to error messages in t ...

issue with node callback function - code malfunctioning

I have written a script in Node.js and Express to send an email after a SQL transaction is successfully completed! router.post('/',function(req,res,next){ sql.connect(config).then(function() { var request = new sql.Request(); ...

Silencing the warning message from ngrx/router-store about the non-existent feature name 'router'

After adding "@ngrx/router-store" to my project, I noticed that it fills the app console in development mode and unit test results with a repeated message: The warning states that the "router" feature name does not exist in the state. To fix this, make ...

Identifying the Back Button in Vue-Router's Navigation Guards

For my specific situation, it is crucial to understand how the route is modified. I need to be able to detect when the route changes due to a user clicking the back button on their browser or mobile device. This is the code snippet I currently have: rout ...

Issues with compiling Vuetify SASS in Nuxt for incorrect output

I am trying to integrate Vuetify with Nuxt using a Plugin but I am encountering the following issue: https://i.sstatic.net/KZbCN.png Error Message: ERROR in ./node_modules/vuetify/src/components/VProgressLinear/VProgressLinear.sass ...

Sequencing API Calls: A Guide on Making Sequential API Requests

Currently, I am working on mastering RxJS. Within my project, there are 3 API calls that need to be made. Specifically, I must execute the 2nd API call and then pass its data as a parameter to the 3rd API call. My attempt at achieving this functionality is ...

JavaScript - Uncaught TypeError: type[totypeIndex] is not defined

After following a tutorial and successfully completing the project, I encountered a JavaScript error saying "Uncaught TypeError: totype[totypeIndex] is undefined". When I tried to log the type of totype[totypeIndex], it initially showed as String, but late ...

Leverage the properties from a slot in Vue to use as local component data

Hello, I am currently using a layout that contains a slot where I render my pages. The layout fetches some data when rendered and passes it to the slot as props. While I can easily access the props within the template, I'm unable to use it as local da ...

Attempting to choose everything with the exception of a singular element using the not() function in Jquery

Within the mosaic (div #mosaique) are various div elements. My goal is to make it so that when I click on a specific div, like #langages or #libraries, the other divs become opaque while the selected div undergoes a size change (which works correctly in t ...

What is the best way to populate an array with unique values while applying a condition based on the objects' properties?

Looking to create a unique exam experience for each student? I have an array of question objects and the goal is to select random and distinct questions from this array until the total sum of scores equals the specified exam score. This means that every s ...

Echarts: Implementing a Custom Function Triggered by Title Click Event

I recently created a bar graph using Echart JS, but I'm struggling to customize the click event on the title bar. I attempted to use triggerEvent, but it only seems to work on statistics rather than the title itself. JSFiddle var myChart = echarts.i ...

Using CSS to position elements absolutely while also adjusting the width of the div

In one section of my website, I have a specific div structure. This structure consists of two divs stacked on top of each other. The first div is divided into two parts: one part with a width of 63% and another part with a button. Beneath the first div, t ...

What is the best way to retrieve the checkbox value using AJAX/jQuery with a Spring form?

My form contains a group of checkboxes identified by the path deliveryStatus, like so: <form:checkbox path="deliveryStatus" value="notDelivered"/> <form:checkbox path="deliveryStatus" value="delivered"/> I came across two helpful examples: E ...

The code function appears to be malfunctioning within the Cordova platform

When working with Cordova, I encountered an issue where I needed to trigger a button event from a listener. In my app, I am loading a specific page . This page contains a button with the class name "btn", and I wanted to display an alert when that button i ...

Enhancing web page interactivity through dynamic element names with Javascript and jQuery

I have an interesting HTML setup that looks like this: <div> <input type="text" name="array[a][b][0][foo]" /> <input type="text" name="array[a][b][0][bar]" /> <select name="array[0][a][b][baz]>...</select> </div> ...