Functions, this and Closures · Lesson 5 of 25
The this Keyword in LWC
Inside a component method, this is the component – so this.count and this.template just work. Then one day you pass a method to setTimeout or window.addEventListener and get "Cannot read properties of undefined". Understanding why takes five minutes and saves hours.
In this lesson
- The rule: this is decided by how a function is called
- Why template handlers work
- Losing this in callbacks – and three fixes
- Adding and removing window listeners correctly
The rule
For a normal function or method, this is whatever is left of the dot at call time. Arrow functions don't have their own this – they use the this of the code around them.
How this is decided
- obj.method()this = obj
- const f = obj.method; f()this = undefined (strict mode) – the dot is gone
- f.bind(obj)a new function with this fixed to obj
- () => this.xarrow: this from the surrounding code
class Counter {
count = 0;
increment() {
this.count++;
return this.count;
}
}
const counter = new Counter();
console.log(counter.increment()); // called with a dot: this = counter
const loose = counter.increment; // the method, detached from its object
try {
loose();
} catch (e) {
console.log(e.name + ': ' + e.message);
}
const bound = counter.increment.bind(counter);
console.log(bound()); // bind fixes this
const viaArrow = () => counter.increment();
console.log(viaArrow()); // the arrow calls it with a dot
1 TypeError: Cannot read properties of undefined (reading 'count') 2 3
Why template handlers just work
When you write onclick={handleClick}, the LWC engine calls your method with this set to the component. That's why you never need bind for handlers declared in the template. Problems only start when you hand a method to someone else.
Losing this in timers and callbacks
// countdownTimer.js
import { LightningElement } from 'lwc';
export default class CountdownTimer extends LightningElement {
seconds = 3;
intervalId;
connectedCallback() {
// ✗ setInterval(this.tick, 100) would call tick() without a dot: this is undefined inside
// ✓ an arrow function keeps the component's this
// eslint-disable-next-line @lwc/lwc/no-async-operation
this.intervalId = setInterval(() => this.tick(), 100);
}
tick() {
this.seconds -= 1;
if (this.seconds === 0) clearInterval(this.intervalId);
}
disconnectedCallback() {
clearInterval(this.intervalId); // always clean up
}
get label() {
return this.seconds > 0 ? `Starting in ${this.seconds}…` : 'Go!';
}
}
<!-- countdownTimer.html -->
<template>
<p>{label}</p>
</template>
Starting in 3… › wait 150ms Starting in 2… › wait 200ms Go!
The same applies to array.map(this.format) if format uses this, and to promise callbacks: write .then((result) => this.handleResult(result)).
About the eslint-disable comment
LWC's lint rule @lwc/lwc/no-async-operation warns on every setTimeout/setInterval, because timers are often used to "wait for rendering" – which is fragile. When a timer is genuinely what you need (a clock, a debounce), disable the rule for that line and make sure you clear it.
Window and document listeners
Listening to events outside your component – window resize, a keyboard shortcut – means calling addEventListener yourself, and removing it when the component goes away. removeEventListener only works with the same function reference, so this.handleKey.bind(this) written twice won't match. Store the bound function in a field:
// shortcutListener.js
import { LightningElement } from 'lwc';
export default class ShortcutListener extends LightningElement {
lastKey = 'none';
// a class field holding an arrow function: one stable reference, this always the component
handleKeyDown = (event) => {
this.lastKey = event.key;
};
connectedCallback() {
window.addEventListener('keydown', this.handleKeyDown);
}
disconnectedCallback() {
window.removeEventListener('keydown', this.handleKeyDown);
}
}
<!-- shortcutListener.html -->
<template>
<p>Last key: {lastKey}</p>
</template>
Forgetting removeEventListener is a real memory leak: the window keeps a reference to the removed component, and its handler keeps running every time the user presses a key.
Arrow-function class fields vs methods
method handle() {} | arrow field handle = () => {} | |
|---|---|---|
| this when passed as a callback | lost | always the component |
| Stored | once, on the prototype | one copy per component instance |
| Best for | template handlers, normal methods | listeners you add and remove yourself |
Practice
Try it yourself
This code throws "Cannot read properties of undefined (reading 'records')". Why, and what's the smallest fix?
getContacts().then(function (data) { this.records = data; });Show answer
A regular
functioncallback gets its ownthis, which is undefined when the promise calls it. Use an arrow:.then((data) => { this.records = data; }).Why doesn't
window.removeEventListener('resize', this.onResize.bind(this))remove the listener?Show answer
bindcreates a new function every time, so it isn't the function that was added. Bind once and store it (or use an arrow-function field).
Quick check
What is this in a detached method call in strict mode?
undefined.
Do arrow functions have their own this?
No – they use the this of the surrounding code.
Do template handlers need bind?
No – LWC calls them with this set to the component.
Summary
- this is set by how a function is called; arrows inherit it.
- Template handlers are safe; callbacks you pass yourself need an arrow or bind.
- Store listeners in a field so add and remove use the same reference.
- Clear timers and remove listeners in disconnectedCallback.
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.