This one came up on night one, about four paragraphs into Week 1. You have written this thousands of times: set properties at the top of a class, read them everywhere else in the class. And here is the plot twist worth saying first: that usage was always correct. You were never reckless. You had a working mental model for 90% of this, and this page is about the missing 10% nobody ever taught you.
The real definition
this is a secret extra parameter that every regular function has. It is not "the class." It is not "the object the function lives in." It gets filled in fresh on every single call, and the call site decides the value. Everything else about this is just the rules for how that parameter gets filled in.
The mental shift is that this is not a property of the function. Two calls to the same function can see two different values, because the value is decided at the moment of calling, not at the moment of writing.
Inside a class
class SpoilerBar {
constructor() {
this.open = false // bolting properties onto the newborn object
}
toggle() {
this.open = !this.open
}
}
const bar = new SpoilerBar()
bar.toggle() // this === barnew creates a fresh empty object and calls the constructor with this pointed at it. That is why "set the variables at the beginning of the class, use them throughout" worked: you were attaching properties to the object being born, then reading them back off the same object.
PHP's $this behaves this way too, which is why this half of this always felt natural. The difference is that PHP's $this is structural, always the object, permanently. JavaScript's is a per-call argument that only usually ends up being the object.
Your entire class-component career lived here, and it held up because React always called instance.render() and instance.componentDidMount() with the dot intact.
Outside a class, on a plain object
const houseguest = {
name: 'Chelsie',
introduce() {
return `Hi, I'm ${this.name}`
},
}
houseguest.introduce() // "Hi, I'm Chelsie"Whatever is left of the dot becomes this. No class, no new, same rule. This is worth seeing on its own because it proves this was never about classes: it is about how the function got called.
On its own, with no dot
This is the case the class-component years never showed you.
function whoAmI() {
return this
}
whoAmI() // undefinedCall a function with nothing in front of it and there is nothing to fill the parameter with, so this is undefined. That is true in ES modules and in strict mode, which is where you live: every file in a Next.js app is a module.
Two wrinkles worth knowing so old code does not confuse you.
In a classic non-strict script, an inline <script> in a WordPress template for instance, a bare call gives you globalThis, the window object, instead of undefined. Same code, different answer, because the file is not a module. This is why ancient tutorials say this is the window and modern ones say it is undefined. They are both right about their own context.
At the top level of a module, outside any function, this is undefined. In a classic script it is globalThis again.
Handed off to something else
const intro = houseguest.introduce
intro() // "Hi, I'm undefined"Same function. Same code. Different this, because the second call has no dot. Assigning the method to a variable does not carry the object along with it: you took the function and left the object behind.
Every burn you ever got was this rule wearing a disguise:
setTimeout(this.tick, 1000) // betrayed
button.addEventListener('click', obj.go) // betrayed
array.map(obj.format) // betrayedNone of those call your method. They take the function as a value, walk away with it, and call it later with no dot in front. The dot rule never applies. This one does. That was your 11pm "cannot read property of undefined."
Forced by hand: bind, call, apply
const bound = houseguest.introduce.bind(houseguest)
bound() // "Hi, I'm Chelsie", even with no dot
houseguest.introduce.call({ name: 'Tucker' }) // "Hi, I'm Tucker"
houseguest.introduce.apply({ name: 'Tucker' }) // same, args as an array.bind(obj) manufactures a new copy of the function with the secret parameter welded in place, so it survives being passed around. .call and .apply invoke immediately with a this you name, differing only in how they take arguments.
This is the rule behind all that this.handleClick = this.handleClick.bind(this) constructor boilerplate from the class-component era. It was pre-binding methods so they could be handed to onClick without losing the instance. You rarely need it today, but now you know what it was for.
Arrows opt out of the whole game
An arrow function has no this of its own at all. Say this inside an arrow and JavaScript looks outward at the enclosing scope, the same way it finds any normal variable. Nothing at the call site can change it, which means calling .bind() on an arrow does nothing.
class SpoilerBar extends React.Component {
handleClick = () => {
this.setState({ open: true }) // always the component, forever
}
}The class-field arrow worked because it was created once, during construction, and captured the this that existed right there. You could hand it to any onClick with no ceremony.
Your habit of "use arrows for callbacks because this gets weird" was not a superstition. It was the professional answer, arrived at through scar tissue instead of theory. Most working JS developers got there the same way.
The gotchas, collected
- A method pulled into a variable loses its object.
const f = obj.methodthenf()givesundefined, notobj. - A regular function nested inside a method does not inherit
this. It gets its own, filled by its own call, which is bare. This is what the oldvar self = thisline at the top of a method was working around. An arrow fixes it properly. - Class methods are not auto-bound. Some frameworks in other languages bind methods for you. JavaScript does not, which is the entire reason the
bindboilerplate existed. - Calling
.bind()twice does not rebind. Once bound, a function ignores later binding attempts.
The one trap on the other side of arrows
Arrows are right for callbacks and wrong for object methods, which is the opposite of what "arrows fix this" suggests:
const houseguest = {
name: 'Chelsie',
introduce: () => `Hi, I'm ${this.name}`, // this is NOT houseguest
}There is no enclosing function here, so the arrow reaches past the object and captures the module's this, which is undefined. The object literal is not a scope. Use the shorthand method form (introduce() {}) for object methods and save arrows for the callbacks that get handed elsewhere.
What TypeScript adds
The hooks era you live in now mostly deleted the problem: function components have no this at all, so there is nothing to betray. But now, for the first time, you know exactly what everyone was working around.