Quirky behavior when using arrow functions in event handlers

Hey all,

In the basic todo app tutorial event handlers are defined using ES6 shorthand syntax like this:

Template.task.events({
  'click .toggle-checked'() {
    Tasks.update(this._id, {
      $set: { checked: ! this.checked },
    });
  },

I personally find this less readable then using an explicit arrow function so I refactored it. Problem being, when I do this, the Tasks.update call no longer works.

// refactor to use arrow function 
Template.task.events({
  'click .toggle-checked': () => {
    // this works
    console.log('test');

    // this does not work
    Tasks.update(this._id, {
      $set: { checked: ! this.checked },
    });
  }
});

I’ve debugged both and the this context is identical in both instances – pointing to an object representation of the clicked Tasks document.

Anyone have thoughts on what may be happening here?

Arrow functions aren’t just a shorter way to write functions, they work differently. With arrow functions “this” will point to the parent context which is not what you want in this situation.

@ciwolsey but then why would the this context be identical in both of the situations above?

within each function body this returns the clicked Task document. I confirmed this in debugging. Hence why I’m so confused. Everything appears to be identical; but when I use an arrow function the Tasks.update call doesn’t run.

Sorry, I was in a rush before and missed some stuff…

Basically you need to first of all get the checked status using: event.target.checked not with this.checked. Try out these two variations to see how they behave differently in regards to context/this. Check the console output for each.

This won’t work, “this” is pointing at the window object because you’re using an arrow function.

Template.task.events({
  'click .toggle-checked': (event) => {
    console.log('Context:', this);
    console.log('Checked:', event.target.checked);

    Tasks.update(this._id, {
      $set: { checked: event.target.checked },
    });
  }
});

Without using an arrow function, “this” points where it should, rather than window:

Template.task.events({
  'click .toggle-checked': function(event) {
    console.log('Context:', this);
    console.log('Checked:', event.target.checked);

    Tasks.update(this._id, {
      $set: { checked: event.target.checked },
    });
  }
});

Still not using an arrow function, but using ES6 shorthand “this” again points where it should:

Template.task.events({
  'click .toggle-checked'(event) {
    console.log('Context:', this);
    console.log('Checked:', event.target.checked);

    Tasks.update(this._id, {
      $set: { checked: event.target.checked },
    });
  }
});

But you cannot use arrow functions in this situation.

As a summary you were making two mistakes:

  • Using arrow functions caused this to point to window.
  • You were checking on this.checked for the checked status when you should be checking event.target.checked. this.checked would get the value from the database, which presumably is why you were notting the value before setting it again, but IMHO you should just set true or false based on the actual value from your checkbox which is on event.target.checked.

Your update call was failing to do what you expected because this._id was undefined due to this pointing at window.

Hope that helps.

3 Likes

PS I had to update my post so make sure you read it from the forum rather than any email.

Thank you! Your explanation was incredible helpful.

I also discovered the source of my initial confusion. When I debugged the arrow function, and inspected this in the console it returned the document object. This was misleading. When I console.logged it out, it returned the window, which is the actual this value at run time, hence why the function wasn’t working.

Super strange. I’ve always assumed using a debug statement “froze” the correct scope context and allowed me to accurately inspect the scope that was evaluated at runtime.

Ah yes I see, confusing indeed.