Watchers can be an incredibly powerful tool.

But once you know when to use them, you still have to figure out how.

So I thought I'd show you an example from a project I've been working on.

Focus management example

I was working on focus management in a menu component, when I had a brain wave.

I realized I could use a watcher to greatly simplify things.

The menu needed to move the focus whenever an arrow key or tab was pressed.

To do this, I had a handler method that looked something like this:

handleKeydown(e) {
  let focusItem;

  if (e.key === 'ArrowDown') {
    focusItem = this.getNextItem();
  } else if (e.key === 'ArrowUp') {
    focusItem = this.getPreviousItem();
  }

  if (focusItem) {
    focusItem.$el.focus();
    this.currentFocus = focusItem;
  }
}

This has been edited to make it simpler for this example, but it worked basically like that.

If the down arrow was pressed we'd move focus to the next item. If it was the up arrow, we'd move focus to the previous item. If it was some other key we wouldn't do anything.

It worked fine for awhile, but then I started adding more functionality.

When the menu is first opened, I needed to move focus to the first element:

mounted() {
  this.items[0].$el.focus();
  this.currentFocus = this.items[0].$el;
}

In some cases I needed to unset our focus entirely, and move the focus back to whatever had the focus before the menu had opened:

closeMenu() {
  this.currentFocus = undefined;
  this.previouslyFocused.$el.focus();
}

More and more places started piling up where I was both updating currentFocus and then calling focus() on an element.

That's when I had my brainwave.

Managing side effects

One of the things that watchers are absolutely spectacular at is managing side effects for you.

A side effect is anything that affects more than just the function it's in.

You can think of a side effect as something that happens to your app.

Modifying the state in a component, updating Vuex state, fetching data, or even timers like setTimeout are all side effects.

These sorts of things can't be put into computed properties, because computed props need to be pure and free from side effects.

But watchers love side effects.

Updating currentFocus is a side effect, but moving focus to a new element is even more side effecty (that's the precise technical term for it 😛).

It doesn't just affect the component, and it doesn't just affect the Vue app.

It affects the whole entire DOM, so it's definitely a major side effect.

Simplifying focus management

So I took focus side effect and shoved it — gently — into a watcher.

It looked like this:

watch: {
  currentFocus(val) {
    if (val) {
      val.$el.focus();
    }
  }
}

Simple and straightforward.

But now I never have to worry about actually updating the focus anywhere else.

I just update what currentFocus is set to, and whenever that changes this watcher's got my back.

So my keyboard handling method now looks like this:c

handleKeydown(e) {
  if (e.key === 'ArrowDown') {
    this.currentFocus = this.getNextItem();
  } else if (e.key === 'ArrowUp') {
    this.currentFocus = this.getPreviousItem();
  }
}

To focus on the first element when the menu opens, I now just do this:

mounted() {
  this.currentFocus = this.items[0].$el;
}

Yeah, I got rid of some code and it's shorter.

But that's not the important part here.

The thing that's important is that I now have half as many things to think about.

Before I was always thinking about how to update currentFocus and then remembering to also call focus() on it.

Now I don't have to.

The watcher takes care of it for me.

It may seem insignificant, but it makes the code easier to understand and think about.

And that means fewer bugs, and a more productive developer (you).