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

Avoid using dot notation with objects and instead use `const` for declaring variables for more

interface obj { bar: string } function randomFunction() { let foo: obj = { bar: "" } foo.bar = "hip" } let snack: obj = { bar: "" } snack.bar = "hop" Upon transcompiling, a warning from tslint pops up: Identifier 'foo' is never reassi ...

What is the best way to apply ngClass to style a JSON object based on its length?

Currently, I am working with a mat-table that displays data from a JSON object. My goal is to filter out all records with an ID of length 5 and then style them using ngClass in my HTML template. How can I achieve this? Below is the code I am working with: ...

What is the best way to verify that all elements within an object are 'false' except for one that is 'true'?

I am working with an object that has multiple boolean fields: type SomeObject = { A: boolean B: boolean C: boolean .... } Is there a simple and efficient method to determine if all other fields (except for a specified one) are set to false? We co ...

Retrieve the value of a property within the same interface

Is there a way to access an interface prop value within the same interface declaration in order to dynamically set types? I am attempting something like this: export type MethodNames = "IsFallmanagerUpdateAllowed" | "UpdateStammFallmanager& ...

Incorporating a particular JavaScript library into Angular 4 (in case the library doesn't have a variable export)

I am attempting to display the difference between two JSON objects in an Angular 4 view. I have been using a library called angular-object-diff, which was originally created for AngularJS. You can view a demo of this library here: Link I have trie ...

What is the reason behind Typescript not narrowing generic union type components when they are eliminated by type guards?

Consider the following scenario where type definitions and function implementations are provided: interface WithNumber { foo: number; } interface WithString { bar: string; } type MyType = WithNumber | WithString; interface Parameter<C extends My ...

Retrieve indexedDb quota storage data

I attempted the code below to retrieve indexedDb quota storage information navigator.webkitTemporaryStorage.queryUsageAndQuota ( function(usedBytes, grantedBytes) { console.log('we are using ', usedBytes, ' of ', grantedBytes, & ...

Is it possible to access NgbdModalContent properties from a different component?

If I have a component with a template containing an Edit button. When the user clicks on it, I want to load another component as a dynamic modal template. The component is named ProfilePictureModalComponent and it includes the Edit button: (Angular code h ...

Tips for correctly decorating constructors in TypeScript

When a class is wrapped with a decorator, the superclasses lose access to that classes' properties. But why does this happen? I've got some code that demonstrates the issue: First, a decorator is created which replaces the constructor of a cla ...

Learn how to set up browser targeting using differential loading in Angular 10, specifically for es2016 or newer versions

Seeking advice on JS target output for compiled Angular when utilizing differential loading. By default, Angular compiles TypeScript down to ES5 and ES2015, with browsers using either depending on their capabilities. In order to stay current, I've b ...

Tips for removing the notification that the .ts file is included in the TypeScript compilation but not actively used

After updating angular to the latest version 9.0.0-next.4, I am encountering a warning message even though I am not using routing in my project. How can I get rid of this warning? A warning is being displayed in src/war/angular/src/app/app-routing.modul ...

The component is failing to store its value within the database

I'm encountering an problem when attempting to save an option in the database. To address this issue, I created a component in Svelte called StatePicker that is responsible for saving US States. However, when I try to save it in the database using a ...

Do Angular 2 component getters get reevaluated with each update?

What advantages do getters offer compared to attributes initialized using ngOnInit? ...

NestJS does not recognize TypeORM .env configuration in production build

Currently, I am developing a NestJS application that interacts with a postgres database using TypeORM. During the development phase (npm run start:debug), everything functions perfectly. However, when I proceed to build the application with npm run build a ...

Resolving the issue of missing properties from type in a generic object - A step-by-step guide

Imagine a scenario where there is a library that exposes a `run` function as shown below: runner.ts export type Parameters = { [key: string]: string }; type runner = (args: Parameters) => void; export default function run(fn: runner, params: Parameter ...

Step-by-step guide on incorporating an external library into Microsoft's Power BI developer tools and exporting it in PBIVIZ format

I'm attempting to create a unique visualization in PowerBI using pykcharts.js, but I'm running into issues importing my pykcharts.js file into the developer tool's console. I've tried including a CDN path like this: /// <reference p ...

Display the initial occurrence from the *ngIf statement

Is there a way to display only the first match from the *ngIf? I am currently using an object loop with *ngFor, where I have multiple items with the same Id but different dates. I need to filter and display only the item with the most recent date and avo ...

Issue with Angular ngStyle toggle functionality not activating

I'm having an issue with toggling my navbar visibility on click of an image. It works the first time but not after that. Can anyone provide some assistance? Link to Code <img id="project-avatar" (click)="toggleNavbar()" width=20, height=20 style= ...

Zod data structure featuring optional fields

Is there a more efficient way to define a Zod schema with nullable properties without repeating the nullable() method for each property? Currently, I have defined it as shown below: const MyObjectSchema = z .object({ p1: z.string().nullable(), p2 ...

What does the typeof keyword return when used with a variable in Typescript?

In TypeScript, a class can be defined as shown below: class Sup { static member: any; static log() { console.log('sup'); } } If you write the following code: let x = Sup; Why does the type of x show up as typeof Sup (hig ...