Skip to content
wannacode

Effects

In short: An effect is code for side actions (a timer, a request, the tab title); in Vue onMounted runs it once the component is on the page, watch runs it when its source changes, and the cleanup stops what was started.

Problem

The banner is gone, the checks are not

The cheese banitsa is out of stock. The buyer presses "Watch the stock" and a banner appears below the button. While the banner is open, the page checks the stock every second. Here the check only writes a line to the console.

Vue

Press "Watch the stock", wait two seconds and press "Stop watching". The banner disappears, yet "Check #3", "Check #4"… keep coming in the console. Open and close the banner once more: now there are two lines a second. The timer belongs to the browser, not to the component. Nobody stopped it.

The idea

Rendering only computes

A component says what should be on the screen: from props and state it computes the markup. That is rendering. The library repeats it whenever it wants to and compares the result with the previous one.

That is why rendering has to be pure: it does nothing except compute the markup. A timer, a request to the server, a change of document.title, a subscription to events on window do something outside. They are side effects.

In <script setup> the code runs once, when the component is created. That is why the timer in the example starts once per opening. But it starts while the component is still being created, and nothing stops it when the component goes away. The template is the rendering: Vue renders it again whenever the data it reads change. It too should only describe what is shown.

A side effect needs two things that rendering does not give: a moment to start and a moment to stop.

The idea

Effect and cleanup

An effect is code that the component runs once it is on the page. The cleanup is the code that stops what was started when the component goes away.

<script setup>
import { onMounted, onUnmounted } from 'vue';
const props = defineProps(['product']);
let id;
onMounted(() => {
id = setInterval(check, 1000);
});
onUnmounted(() => clearInterval(id));
</script>
  • onMounted(function) tells Vue: "run it when the component is on the page".
  • onUnmounted(function) is called when the component leaves the page. That is the cleanup.
  • id lives outside both functions, because both need to see it.

The cleanup mirrors the effect: setInterval and clearInterval, addEventListener and removeEventListener, a request and cancelling that request.

Try it

Stop the checks

The timer in StockWatcher starts right in the component, and nobody stops it. Move it into an effect that starts when the banner appears, and stop it when the banner goes away. The check opens the banner, waits for "Check #1", closes it and makes sure "Check #2" never comes.

Vue

The idea

Dependencies

Now the buyer can pick another out-of-stock product while the banner is open. The effect checks product. As soon as the product changes, the old check has to stop and a new one has to start for the new product.

That is what watch is for. It watches a source and calls its function when the source changes:

watch(() => props.product, (product, oldProduct, onCleanup) => {
const id = setInterval(check, 1000);
onCleanup(() => clearInterval(id));
}, { immediate: true });
  • The source is a function that returns the value: () => props.product. Vue sees what it reads and knows when to call it again.
  • { immediate: true } runs the function right away too, for the starting product. Without it, watch waits for the first change.
  • onCleanup records the cleanup of this run. Vue calls it before the next run and when the component goes away. Watchers from <script setup> stop with the component on their own.

watch replaces both functions from the last task: onMounted runs once, while watch runs for every new product.

See it

See the life of an effect

The banner opens for the banitsa, the buyer picks the cucumbers, then closes the banner.

<script setup>
const props = defineProps(['product']);
watch(() => props.product, (product, old, onCleanup) => {
let checks = 0;
const id = setInterval(() => {
checks++;
console.log(`Check #${checks}: ${product.name}`);
}, 1000);
onCleanup(() => clearInterval(id));
}, { immediate: true });
</script>
<template>
<p role="status">Watching the stock of {{ product.name }}</p>
</template>
Step by step, in words
  1. Render: Vue builds the markup from the template. The template only describes what to show. It starts nothing.
  2. immediate: true runs the watcher once right away, as the component is created. The timer starts and counts the checks.
  3. The buyer picks another product. The getter () => props.product now returns a different value, so the watcher has to run again.
  4. Before the new run, the cleanup of the old one: its timer stops. Without it the old checks would go on next to the new ones.
  5. The effect runs again with the new product. Its own timer counts from #1.
  6. The buyer closes the banner: the component leaves the page. The last cleanup runs, and the timer stops with it.
  7. Every run has a pair: whatever the effect starts, its cleanup stops. Before the next run and when the component leaves.

The effect runs twice and the cleanup runs twice: once before the new run, once when the component is removed. In between there is always exactly one timer.

Try it

Find the bug

The banner opens, but instead of checks the console shows a warning and TypeError: Cannot read properties of undefined (reading 'name') every second. Which line is to blame?

Pick the line with the bug.

Try it

Fill in

What goes in the blank so that the timer stops when the component goes away?

let id;
onMounted(() => {
  id = setInterval(check, 1000);
});
(() => clearInterval(id));

The idea

When you do not need an effect

An effect is for a link to something outside the component: a timer, a server, the tab title, window. Two things look like an effect, but are not.

  • A value computed from others. The cart total is not kept in a separate piece of state that an effect updates. It is computed during render with computed, as in the lesson on derived state. An effect that writes it into separate state is extra work and a second place that can drift away from the cart.
  • A response to an action. The order is sent because the buyer pressed "Order". That goes in the click handler, not in an effect that watches some flag.

What is left is the real job of an effect: to keep the component in sync with the outside world while the component is on the page.

More precisely, if you are curious

watchEffect. watchEffect((onCleanup) => { … }) starts right away and watches everything its function reads. You do not list the sources, but you also cannot see at a glance what starts it. watch with an explicit source is clearer when you need to know exactly when it runs.

onWatcherCleanup. Since Vue 3.5 the cleanup can also be recorded with onWatcherCleanup(() => …), imported from 'vue'. It has to be called before the first await in the function.

When immediate runs. A function with { immediate: true } runs while the component is being created, before it is in the DOM. A timer and a request do not need the DOM. If the effect has to touch elements from the template, the place is onMounted.

The Options API has the same: mounted, unmounted and the watch option.

Exercise

Another product

Above the banner there is a "Product" drop-down with the banitsa and the cucumbers. Right now StockWatcher starts the timer once, when it is shown. Make it so that when the product changes, the old check stops and a new one starts from #1 for the new product. The check opens the banner, waits for "Check #1" for the banitsa, picks the cucumbers, waits for "Check #1" for them, makes sure "Check #2" for the banitsa never comes, and closes the banner.

Vue

Test

Check yourself

1. A component starts setInterval and never stops it. The buyer opens and closes the banner three times. How many timers are running?
2. The watcher is watch(() => props.product, …), without { immediate: true }. When does the first check start?
3. When does the cleanup of an effect run?
4. Which of these is NOT a job for an effect?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·