Vue 3 Composition API - watch and watchEffect

Vue 3 Composition API - watch and watchEffect

Introduction

One of the major benefits to the new Composition API in Vue 3 is how it provides a flexible and powerful reactivity system to developers. With Vue 2, developers were reliant on the Options API to build single-file components, which enforced a level of structure to how the system was used. The Composition API, on the other hand, gives the developer new opportunities to build reactive applications.

In a previous article, we explored the difference between ref and reactive methods. Let's build on that understanding, and introduce two new methods from the Composition API - watch and watchEffect.

Options API - watch in the Options API

The Options API provided us with the watch option, which is a way to observe when values changed and perform side effects based on that. Here's a basic example of what a watcher could look like with the Options API:

<template>
  <h2>Notes:</h2>
  <textarea v-model="notes" />
</template>

<script lang="ts">
import { defineComponent } from "vue";
import { save } from "./api";

export default defineComponent({
  data() {
    return {
      notes: ""
    }
  },
  watch: {
    notes(value, oldValue) {
      save(value);
    }
  }
})
</script>

Let's summarize what's going on here:

In our template, we have a textarea that is bound to our data value of notes. In our Javascript, we have a watch key, which takes an object. In that object, we have a function, also called notes. This function will be automatically called whenever the value of notes changes (for example, when text is entered into the field).

Each watcher takes two arguments: the new value, and the previous value. Since it's a function, we can do any validation or checks that need to happen here, and perform any side effects. In our example, when the text in the notes field changes, we are saving the value to a backend.

For further reading on watchers with the Options API, here's a link to the documentation for Vue 2.

Now that we have a basic understanding of watchers in the Options API, let's dive into the Composition API!

watch and watchEffect

The Composition API provides us with two different methods to handle side effects - watch and watchEffect. Just like with ref and reactive, these methods are not meant to compete with each other, but to be used as required by the application. Think of them as two similar tools that are each useful in certain cases. Both of these methods do the following:

  • Watch for changes in reactive variables
  • Allow the developer to perform side effects
  • Provide a way to cancel a side effect (in case the state has become invalid)

However, there are a number of differences between them as well. Here's a short list of what makes them different:

  • watch can be used to lazily trigger side effects (watchEffect is always immediate).
  • watchEffect automatically watches for changes to any state changes (watch must be provided with a variable or variables to watch).
  • watch provides access to the current and previous values.

It's important to take into account what you want to achieve, and use the correct tool for the job. As we work through this article, we'll do some comparison against the two options.

Composition API - watchEffect

Let's start refactoring the example above using watchEffect. Below is the same application using the Composition API:

<template>
  <h2>Notes:</h2>
  <textarea v-model="notes" />
</template>

<script lang="ts">
import { watchEffect, ref, defineComponent } from "vue";
import { save } from "./api";

export default defineComponent({
  setup() {
    const notes = ref("");

    watchEffect(() => save(notes.value));

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

Since we are using the Composition API, we use ref to instantiate the notes variable. Also, we need to use notes.value, since it is a reference to the value and not the value itself. Finally, we return an object with notes so that it is available in the template.

With watchEffect, we provide an anonymous function as an argument, then perform our side effect of saving the text. Note that we don't need to provide notes as a value to be watched - watchEffect is capable of watching any reactive variables that are referenced within the callback function.

To highlight this, let's add a new variable to the mix - notesArePublic. Below is our updated application using two variables inside of the watchEffect:

<template>
  <h2>Notes:</h2>
  <textarea v-model="notes" />
  <label>
    Make notes public:
    <input type="checkbox" v-model="notesArePublic" />
  </label>
</template>

<script lang="ts">
import { watchEffect, ref, defineComponent } from "vue";
import { save } from "./api";

export default defineComponent({
  setup() {
    const notes = ref("");
    const notesArePublic = ref(false);

    watchEffect(() =>
      save({
        notes: notes.value,
        notesArePublic: notesArePublic.value,
      })
    );

    return {
      notes,
      notesArePublic,
    };
  },
});
</script>

Now, whenever either notes or notesArePublic change, the side effect will be triggered and our save function will be called. This can cut down on a lot of code compared to the Options API, where you would need to create multiple watchers that do the same thing.

A feature of the Composition API is the ability to remove watchers programmatically. Let's say that our user is done editing their notes, and wants to stop submitting values to the database. Both watch and watchEffect return a function that can be called to stop watching for changes.

Let's update our code to include a button that turns off our watcher.

<template>
  <h2>Notes:</h2>
  <textarea v-model="notes" />
  <label>
    Make notes public:
    <input type="checkbox" v-model="notesArePublic" />
  </label>
  <button @click="saveAndClose">Finish editing</button>
</template>

<script lang="ts">
import { watchEffect, ref, defineComponent } from "vue";
import { save } from "./api";

export default defineComponent({
  setup() {
    const notes = ref("");
    const notesArePublic = ref(false);

    const stopSaving = watchEffect(() =>
      save({
        notes: notes.value,
        notesArePublic: notesArePublic.value,
      })
    );

    const saveAndClose = () => {
      stopSaving();
    };

    return {
      notes,
      notesArePublic,
      saveAndClose,
    };
  },
});
</script>

When we call watchEffect, we are now saving its return as a variable, stopSaving. When the user clicks our Finish editing button, a function is called that will disable the watcher. Nice!

This can be useful when your application is watching for a certain critera to be met. Once the state is how you are watching it to be, you can stop watching, preventing your side effects from being triggered when they shouldn't be. This can help your code stay organized, and clearly communicates to other developers that a watcher is only neccessary for a certain task.

Another great feature is the ability to invalidate our side effects. In our example, what if the user enters more text after the save function has been called? By using the built-in invalidation, we can be aware when something has changed in our state and cancel our API request. Let's take a look at what that would look like.

<template>
  <h2>Notes:</h2>
  <textarea v-model="notes" />
  <label>
    Make notes public:
    <input type="checkbox" v-model="notesArePublic" />
  </label>
  <button @click="saveAndClose">Finish editing</button>
</template>

<script lang="ts">
import { watchEffect, ref, defineComponent } from "vue";
import { save } from "./api";

export default defineComponent({
  setup() {
    const notes = ref("");
    const notesArePublic = ref(false);

    const stopSaving = watchEffect((onInvalidate) => {
      const cancel = save({
        notes: notes.value,
        notesArePublic: notesArePublic.value,
      });

      onInvalidate(() => cancel());
    });

    const saveAndClose = () => {
      stopSaving();
    };

    return {
      notes,
      notesArePublic,
      saveAndClose,
    };
  },
});
</script>

The callback function we passed into watchEffect now has an argument - onInvalidate. This function also takes a callback as an argument, which is called when the watched state has changed. In our example, the save function is now returning a function that we are calling cancel, which can be used to abort the API request. When onInvalidate triggers, we call cancel, aborting the API request. No need to wait for the response when it's already out of date!

One thing to keep in mind is that watchEffect is immediately firing when your app loads. From the documentation, "To apply and automatically re-apply a side effect based on reactive state, we can use the watchEffect method. It runs a function immediately while reactively tracking its dependencies and re-runs it whenever the dependencies are changed." This means that immediately upon loading the page, our side effect is being triggered and data is sent to our API. This is very important to keep in mind! You may not want certain side effects (like saving an empty text field) to happen. If you need to lazily trigger effects, use the watch method instead.

Another important note: watchEffect is not watching your variables deeply. If we had a single object of data, which contained both of our variables, those variables updating would not trigger the side effect. In this case, we could convert the reactive object to refs, which would then correctly trigger our side effect.

<template>
  <h2>Notes:</h2>
  <textarea v-model="data.notes" />
  <label>
    Make notes public:
    <input type="checkbox" v-model="data.notesArePublic" />
  </label>
  <button @click="saveAndClose">Finish editing</button>
</template>

<script lang="ts">
import { watchEffect, reactive, defineComponent, toRefs } from "vue";
import { save } from "./api";

export default defineComponent({
  setup() {
    const data = reactive({
      notes: "",
      notesArePublic: false,
    });

    const stopSaving = watchEffect((onInvalidate) => {
      // This doesn't work
      // const cancel = save(data);

      // This does
      const parsed = toRefs(data);

      const cancel = save({
        notes: parsed.notes.value,
        notesArePublic: parsed.notesArePublic.value
      })

      onInvalidate(() => cancel());
    });

    const saveAndClose = () => {
      stopSaving();
    };

    return {
      data,
      saveAndClose,
    };
  },
});
</script>

Composition API - watch

Now let's explore the watch method. We can improve it by adding some feedback to the user when their notes have been saved. To do this, we will add a new variable: showNotesSavedNotification, but we'll only show it for a specific amount of time.

<template>
  <h2>Notes:</h2>
  <textarea v-model="notes" />
  <label>
    Make notes public:
    <input type="checkbox" v-model="notesArePublic" />
  </label>
  <button @click="saveAndClose">Finish editing</button>
  <aside v-if="showNotesSavedNotification">Notes have been saved!</aside>
</template>

<script lang="ts">
import { watchEffect, watch, ref, defineComponent } from "vue";
import { save } from "./api";

export default defineComponent({
  setup() {
    const notes = ref("");
    const notesArePublic = ref(false);
    const showNotesSavedNotification = ref(false);

    const stopSaving = watchEffect((onInvalidate) => {
      const cancel = save({
        notes: notes.value,
        notesArePublic: notesArePublic.value
      }, () => showNotesSavedNotification.value = true)

      onInvalidate(() => cancel());
    });

    const saveAndClose = () => {
      stopSaving();
    };

    watch(showNotesSavedNotification, (value) => {
      if (value) {
        setTimeout(() => {
          showNotesSavedNotification.value = false;
        }, 5000);
      }
    })

    return {
      notes,
      notesArePublic,
      showNotesSavedNotification,
      saveAndClose,
    };
  },
});
</script>

Using the watch method is very similar to how we would use functions with the watch option in the Options API. In fact, according to the documentation, "The watch API is the exact equivalent of the component watch property." In our example above, whenever content is saved, the showNotesSavedNotification variable is set to true. Our watcher is then called, setting a timeout and clearing the notification after five seconds.

Let's go back to the point I made before about watchEffect being immediate. We don't really want to save an empty text field to our database - it's much more reasonable to wait for the user to enter a value first. Let's try using watch instead of watchEffect and see what benefits we get from it.

<template>
  <h2>Notes:</h2>
  <textarea v-model="notes" />
  <label>
    Make notes public:
    <input type="checkbox" v-model="notesArePublic" />
  </label>
  <button @click="saveAndClose">Finish editing</button>
  <aside v-if="showNotesSavedNotification">Notes have been saved!</aside>
</template>

<script lang="ts">
import { watch, ref, defineComponent } from "vue";
import { save } from "./api";

export default defineComponent({
  setup() {
    const notes = ref("");
    const notesArePublic = ref(false);
    const showNotesSavedNotification = ref(false);

    const stopSaving = watch(
      [notes, notesArePublic],
      (value, oldValue, onInvalidate) => {
        const cancel = save(
          {
            notes: notes.value,
            notesArePublic: notesArePublic.value,
          },
          () => (showNotesSavedNotification.value = true)
        );

        onInvalidate(() => cancel());
      }
    );

    const saveAndClose = () => {
      stopSaving();
    };

    watch(showNotesSavedNotification, (value) => {
      if (value) {
        setTimeout(() => {
          showNotesSavedNotification.value = false;
        }, 5000);
      }
    });

    return {
      notes,
      notesArePublic,
      showNotesSavedNotification,
      saveAndClose,
    };
  },
});
</script>

We have removed watchEffect and replaced it with watch. Did you notice that the first argument to the method is an array with both of our values? We can use this syntax to watch multiple variables, rather than being tied to a single one. In this way, we still only have to write a single watcher, which is excellent for avoiding code duplication.

Also of note - because we are watching an array, the values of value and oldValue are also arrays. For example, on the first keypress, oldValue would look like [ "", false ]. Depending on your use case, this might not be particularly helpful, but it's important to be aware of if you need to track the previous values of your watched variables.

In addition, the watch method is still providing us with the ability to remove the watcher, as well as the onInvalidate method. This is another improvement over the Options API. Regardless of whether we use watchEffect or watch, we still get the benefits of the improved API.

Let's make one last change to our application. I'd really like to bundle our notes and public status into a reactive object. Does that work with watch better than it did with watchEffect? Let's find out.

<template>
  <h2>Notes:</h2>
  <textarea v-model="data.notes" />
  <label>
    Make notes public:
    <input type="checkbox" v-model="data.notesArePublic" />
  </label>
  <button @click="saveAndClose">Finish editing</button>
  <aside v-if="showNotesSavedNotification">Notes have been saved!</aside>
</template>

<script lang="ts">
import {
  watch,
  ref,
  reactive,
  defineComponent,
} from "vue";
import { save } from "./api";

export default defineComponent({
  setup() {
    const data = reactive({
      notes: "",
      notesArePublic: false,
    });
    const showNotesSavedNotification = ref(false);

    const stopSaving = watch(
      () => ({...data}),
      (value, oldValue, onInvalidate) => {
        console.log(value, oldValue)
        const cancel = save(
          value,
          () => (showNotesSavedNotification.value = true)
        );

        onInvalidate(() => cancel());
      }
    );

    const saveAndClose = () => {
      stopSaving();
    };

    watch(showNotesSavedNotification, (value) => {
      if (value) {
        setTimeout(() => {
          showNotesSavedNotification.value = false;
        }, 5000);
      }
    });

    return {
      data,
      showNotesSavedNotification,
      saveAndClose,
    };
  },
});
</script>

Yes, in fact, we can watch our reactive object! According to the documentation, "A watcher data source can either be a getter function that returns a value, or directly a ref". In this case, since we are using a reactive object, we can pass in a function that returns our data reactive object, and the watcher will trigger as expected.

Be aware that we technically could watch the data object directly, but this can have unintended consequences. Again from the docs, "Watching a reactive object or array will always return a reference to the current value of that object for both the current and previous value of the state. To fully watch deeply nested objects and arrays, a deep copy of values may be required."

In this case, if we were to do this:

const stopSaving = watch(
  data,
  (value, oldValue, onInvalidate) => {
    console.log(value, oldValue)
    const cancel = save(
      value,
      () => (showNotesSavedNotification.value = true)
    );

    onInvalidate(() => cancel());
  },
);

Then both value and oldValue would be identical! This is clearly not what we meant to do. To avoid this, we are using the Spread syntax to create a new object which is both updating and being watched correctly, as well as not be passed by reference into both the current and previous values.

One more point to be aware of: because the watch method is the same as what is provided in the Options API, we also have access to its options, like deep and immediate. So if you need to watch an object deeply, or trigger side effects immediately, you can still do so with the watch method!

Conclusion

Both watch and watchEffect have their uses. Both of them can be used to trigger side effects in your application. Next time you find yourself needing to trigger an effect in your code, ask yourself:

  • Does this effect need to be immediate?
  • Should it only trigger from a single source, or whenever its dependencies change?
  • Am I watching an object, or a ref?

And of course, make sure that a side effect is really the correct course of action for your use case. Side effects can often be used when either a computed property, or a method is a better choice for the situation.

Here's a link to an example of the final version of this code on Stackblitz.

One last thing - because the Composition API doesn't need to be used within Vue single-file components, there are a lot of great uses for both watch and watchEffect. Here's a great presentation from VueConf Toronto where Oscar Spencer uses the Vue Composition API with an Express app to trigger side effects on Twitter. Check it out!

Until next time!

This Dot Labs is a modern web consultancy focused on helping companies realize their digital transformation efforts. For expert architectural guidance, training, or consulting in React, Angular, Vue, Web Components, GraphQL, Node, Bazel, or Polymer, visit thisdotlabs.com.

This Dot Media is focused on creating an inclusive and educational web for all. We keep you up to date with advancements in the modern web through events, podcasts, and free content. To learn, visit thisdot.co.

You might also like