this kept breaking my code, so i learned binding
Early in my first React job, I built a class component with a button. Click the button, the counter goes up. Very simple, maybe 20 lines. I clicked it and got TypeError: Cannot read properties of undefined. I stared at that error for a solid 40 minutes before the senior dev on my team walked over, typed .bind(this) in the constructor, and walked away like a surgeon closing an incision. I had no idea why it worked. I just shipped it and moved on.
For about two years after that, I treated binding like a superstition. See a class method, bind it, ask questions never. (It worked every time, which is the most dangerous kind of not understanding.) Anyways, this post is the explanation I wish someone had given me back then, plus the arrow-function-era update.
The one rule about this
this is decided by how a function is called, not where it is written. Read that again, because it is the whole post. When you write user.greet(), JavaScript sees the dot at the call site and sets this to user. When you write const f = user.greet; f(), there is no dot at the call site, so this is undefined (in strict mode, which is every module you will write in 2026). Detaching a method from its object strips its this off. That is it. That is the bug, every time.
const user = {
name: 'Nayem',
greet() {
return 'Hi, I am ' + this.name
},
}
console.log(user.greet()) // "Hi, I am Nayem". The dot does the work.
const greetAlone = user.greet
console.log(greetAlone()) // TypeError. No dot, no this.Press a button.
Try it. Same function, same object. One button calls it with the dot, one calls it without. The error message is the one that used to ruin my afternoon.
Where this bites you in real code
It is almost never the object-literal case that gets you. It is the callback case, where the function gets detached from its object silently, three files away from where the bug shows up:
- Event handlers.
onClick={this.handleClick}hands React the function without the object. React calls it later, dotless. - Timers.
setTimeout(user.save, 1000)does the same thing, just with a delay, so the stack trace arrives when you have already forgotten what you wrote. - Array methods.
items.map(this.renderItem)passes the method along andthisnever makes the trip.
Notice the pattern. You never wrote a bad function. You wrote a good function and then handed it to someone else, and the handoff dropped the this. (Like lending a friend your car without the keys. The car is fine.)
bind, call, apply (the old school fixes)
bind returns a new function with this locked in. call and apply run the function immediately with the this you hand them (apply takes an array of arguments, call takes them one by one). The full reference lives on MDN, but here is the 30-second version:
const user = {
name: 'Nayem',
greet(greeting) {
return greeting + ', I am ' + this.name
},
}
// bind: lock this in, call the result whenever
const bound = user.greet.bind(user)
bound('Hello') // "Hello, I am Nayem"
// call: run it once, right now, with this set
user.greet.call({ name: 'Alex' }, 'Hey') // "Hey, I am Alex"
// apply: same as call, but arguments arrive as an array
user.greet.apply({ name: 'Alex' }, ['Yo']) // "Yo, I am Alex"Why arrow functions made most of this go away
Arrow functions have no this of their own. They borrow it from where they are written, permanently. So onClick={() => this.handleClick()} keeps the dot at the call site, and this survives. Same with setTimeout(() => user.save(), 1000). This is why the whole binding problem quietly faded when everyone moved to function components and hooks: we just stopped writing the code that triggers it.
One gotcha survives, though. Arrow functions make bad object methods, because their this comes from the surrounding scope, not the object:
const counter = {
count: 0,
// arrow function: this is the module scope, not counter
tick: () => {
this.count++ // TypeError. Surprise.
},
// regular method: the dot does the work, as usual
tickFixed() {
this.count++
},
}The rule of thumb I actually use: arrow functions for callbacks, regular methods for objects. Each one puts this where you expect it.
The React version of this story
This is where my 40-minute stare-down happened. Class components need every handler bound, because onClick={this.handleClick} is exactly the detaching-a-method case from above. The classic fix is one line in the constructor:
class Counter extends React.Component {
constructor(props) {
super(props)
this.state = { count: 0 }
// the line that haunted me for two years
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
this.setState({ count: this.state.count + 1 })
}
render() {
return (
<button onClick={this.handleClick}>
Clicked {this.state.count} times
</button>
)
}
}The shorter modern version is an arrow function as a class property, which keeps this as the component automatically because arrow functions borrow it from where they were written:
class Counter extends React.Component {
state = { count: 0 }
// arrow function, so this is the component. No bind needed.
handleClick = () => {
this.setState({ count: this.state.count + 1 })
}
render() {
return (
<button onClick={this.handleClick}>
Clicked {this.state.count} times
</button>
)
}
}So, what have I learnt?
- Look at the call, not the definition. If there is a dot before the function name when it runs,
thisis the thing before the dot. If not,thisis gone. - A method passed as a callback arrives with its
thisstripped off. That handoff is where almost every one of these bugs lives. bindlocksthisin for later,callandapplyrun once with thethisyou give them, and arrow functions borrowthisfrom where they were written.- I cargo-culted
.bind(this)in constructors for two years without knowing what it did, and the code worked the whole time. Understanding it did not make my code better. It just means I can read old class components without that lowkey dread, which, honestly, is the entire payoff.