State: The Invisible Puppeteer
Master React state management with this comprehensive guide. Learn why regular variables don't trigger re-renders, how this.setState works in class components, and common pitfalls like async state updates. Perfect for understanding React's core concept.
Previously on “What The Heck Is…”
In my last post, I finally understood what components were. Little reusable pieces of UI. LEGO blocks for the web. I felt unstoppable.
Then I tried to make a counter.
The Counter That Wouldn’t Count
Here’s what I thought would work:
class Counter extends React.Component {
render() {
let count = 0;
function handleClick() {
count = count + 1;
console.log(count); // This logs 1, 2, 3...
}
return (
<div>
<p>Count: {count}</p> {/* But this always shows 0 */}
<button onClick={handleClick}>Add One</button>
</div>
);
}
}
I clicked the button. The console showed 1. The screen showed 0.
I clicked again. Console: 2. Screen: 0.
What kind of sorcery is this?
The Revelation
Here’s what I didn’t understand: React doesn’t know your variable changed.
When you update a regular JavaScript variable, React just… doesn’t care. It has no idea anything happened. Your component rendered once with count = 0, and as far as React is concerned, that’s the end of the story.
To make React react (pun very much intended), you need to use state.
Enter: this.state and this.setState
In 2019, we were still in the class component era. Here’s how state worked:
class Counter extends React.Component {
constructor(props) {
super(props);
// Initialize state in the constructor
this.state = {
count: 0
};
// Don't forget to bind your methods!
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// This tells React: "Hey! Something changed!"
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<div>
<p>Count: {this.state.count}</p> {/* Now this updates! */}
<button onClick={this.handleClick}>Add One</button>
</div>
);
}
}
When you call this.setState(), you’re not just updating a variable. You’re sending a signal to React: “Something changed. Please re-render this component with the new value.”
The Mental Model That Finally Clicked
Think of it like this:
Regular variables are like writing on a whiteboard that nobody’s looking at. You can change it all you want, but if no one checks the board, nothing happens.
State is like writing on a whiteboard with an alarm attached. Every time you write something new, the alarm goes off and everyone looks at the board.
// Regular variable - silent whiteboard
let count = 0;
count = 1; // Changed, but no one cares
// State - alarm whiteboard
this.state = { count: 0 };
this.setState({ count: 1 }); // ALARM! React re-renders!
The Binding Nightmare
Oh, the number of hours I lost to this being undefined:
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
// If you forget this line...
// this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// TypeError: Cannot read property 'setState' of undefined
this.setState({ count: this.state.count + 1 });
}
render() {
return <button onClick={this.handleClick}>Click me</button>;
}
}
Without binding, this inside handleClick is undefined. Why? Because JavaScript is weird about this in callbacks.
There were a few ways to fix this:
// Option 1: Bind in constructor (cleanest)
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
// Option 2: Arrow function in render (creates new function each render)
render() {
return <button onClick={() => this.handleClick()}>Click</button>;
}
// Option 3: Class property with arrow function (my favorite)
handleClick = () => {
this.setState({ count: this.state.count + 1 });
}
State Updates Are Asynchronous
This one bit me HARD:
handleClick() {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // Still shows the OLD value!
}
setState doesn’t update immediately. It schedules an update. If you need to do something after state updates, use the callback:
handleClick() {
this.setState(
{ count: this.state.count + 1 },
() => {
console.log(this.state.count); // NOW it's updated
}
);
}
The setState Gotcha with Previous State
Here’s a bug that confused me for days:
handleTripleClick() {
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
// Expected: count increases by 3
// Actual: count increases by 1!
}
Because state updates are async and batched, all three calls see the same initial this.state.count. The fix? Use the function form:
handleTripleClick() {
this.setState(prevState => ({ count: prevState.count + 1 }));
this.setState(prevState => ({ count: prevState.count + 1 }));
this.setState(prevState => ({ count: prevState.count + 1 }));
// Now count increases by 3!
}
State Is Private
Here’s something that confused me: state belongs to a specific instance of a component.
class App extends React.Component {
render() {
return (
<div>
<Counter /> {/* Has its own count */}
<Counter /> {/* Has its own count */}
<Counter /> {/* Has its own count */}
</div>
);
}
}
Each Counter has its own independent state. Clicking one doesn’t affect the others. They’re like separate apartments in the same building—same layout, different residents.
State vs Props: The Eternal Confusion
For weeks, I mixed these up constantly. Here’s the difference:
Props = Data passed DOWN from a parent. Read-only. Like receiving a letter.
State = Data managed INSIDE a component. Can change. Like your own notebook.
class Greeting extends React.Component {
constructor(props) {
super(props);
this.state = { clickCount: 0 };
}
handleClick = () => {
this.setState(prev => ({ clickCount: prev.clickCount + 1 }));
}
render() {
return (
<div onClick={this.handleClick}>
Hello, {this.props.name}! {/* name = prop (from parent) */}
Clicked {this.state.clickCount} times {/* clickCount = state (managed here) */}
</div>
);
}
}
What I Wish Someone Had Told Me
-
Always use setState(), never modify
this.statedirectly.this.state.count = 5won’t trigger a re-render. -
State updates are merged. If your state is
{ name: 'Saurav', age: 25 }and you callsetState({ age: 26 }), the name stays. You don’t need to spread. -
Use the function form of setState when the new state depends on the previous state.
-
State should be minimal. Store the least amount of information needed. Derive everything else.
-
Bind your methods or use arrow functions. Future you will thank present you.
The Journey Continues
Understanding state was a major milestone. Suddenly, I could build things that actually did something. Forms that remembered input. Counters that counted. Toggle buttons that toggled.
But then I tried to share state between distant components, and discovered the hellscape known as “prop drilling.”
That’s a story for next time.
P.S. — I hear there’s something called “hooks” coming that makes all this binding nonsense go away. Can’t wait to see what that’s about…
Saurav Sitaula
Software Architect • Nepal