State: The Invisible Puppeteer

SS Saurav Sitaula

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

  1. Always use setState(), never modify this.state directly. this.state.count = 5 won’t trigger a re-render.

  2. State updates are merged. If your state is { name: 'Saurav', age: 25 } and you call setState({ age: 26 }), the name stays. You don’t need to spread.

  3. Use the function form of setState when the new state depends on the previous state.

  4. State should be minimal. Store the least amount of information needed. Derive everything else.

  5. 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…

SS

Saurav Sitaula

Software Architect • Nepal

Back to all posts
Saurav.dev

© 2026 Saurav Sitaula.AstroNeoBrutalism