11_React State — Complete Guide (Functional & Class) - Maniconserve/React-Wiki GitHub Wiki

State is data that belongs to a component and can change over time. When state changes, React automatically re-renders the component to show the updated UI.

  Props State
Where it comes from Parent component Inside the component itself
Can it change? ❌ Read-only ✅ Yes
Who controls it? Parent The component itself

Part of the React Project Wiki — see also: React Components · Fragments & Event Handling

# React State — Complete Guide (Functional & Class)

State is data that belongs to a component and can change over time. When state changes, React automatically re-renders the component to show the updated UI.

Props State
Where it comes from Parent component Inside the component itself
Can it change? ❌ Read-only ✅ Yes
Who controls it? Parent The component itself

The Problem Without State

Regular JavaScript variables do not trigger a re-render. React does not watch them.

// ❌ This does NOT work — UI never updates
function Counter() {
  let count = 0;

  function handleClick() {
    count = count + 1;
    console.log(count); // value changes in memory
    // but React doesn't know — screen stays at 0
  }

  return (
    <div>
      <p>{count}</p>
      <button onClick={handleClick}>Add</button>
    </div>
  );
}

The variable updates internally but React has no idea — it never re-renders the component. This is the problem both useState and this.state solve.


Part 1 — Functional Component State (useState)

useState is a React Hook that creates a state variable. When you update it using the setter function, React automatically re-renders the component.

Syntax

const [value, setValue] = useState(initialValue);
Part What it is
value The current state value
setValue Function to update the state
initialValue The starting value on first render

This uses array destructuringuseState returns an array of two items and we name them whatever we want.


Basic Example — Counter

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1); // updates state → React re-renders
  }

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Add</button>
    </div>
  );
}

Step by step:

  1. Component renders — count is 0
  2. User clicks — handleClick runs
  3. setCount(count + 1) is called — state updates to 1
  4. React re-renders — screen shows Count: 1

Initial Values

useState accepts any JavaScript value.

const [count, setCount]         = useState(0);
const [name, setName]           = useState('');
const [isVisible, setIsVisible] = useState(false);
const [user, setUser]           = useState(null);
const [items, setItems]         = useState([]);
const [form, setForm]           = useState({ email: '', password: '' });

String State — Controlled Input

Binding an input's value to state is called a controlled input. State is the single source of truth for what the input shows.

import { useState } from 'react';

function NameInput() {
  const [name, setName] = useState('');

  return (
    <div>
      <input
        type="text"
        value={name}                              // state → input
        onChange={e => setName(e.target.value)}   // input → state
        placeholder="Enter your name"
      />
      <p>Hello, {name}!</p>
    </div>
  );
}

Two-way binding:

  • value={name} — state controls what the input displays
  • onChange — every keystroke updates state

Boolean State — Toggle

import { useState } from 'react';

function ToggleBox() {
  const [isVisible, setIsVisible] = useState(false);

  return (
    <div>
      <button onClick={() => setIsVisible(!isVisible)}>
        {isVisible ? 'Hide' : 'Show'}
      </button>
      {isVisible && <p>Now you see me!</p>}
    </div>
  );
}

!isVisible flips the boolean — true becomes false and vice versa.


Object State — Form

When state is an object, always spread the existing state and only override the changed field. If you don't, you will overwrite the entire object and lose all other fields.

import { useState } from 'react';

function LoginForm() {
  const [form, setForm] = useState({ email: '', password: '' });

  function handleChange(event) {
    setForm({
      ...form,                                    // keep existing fields
      [event.target.name]: event.target.value     // update only changed field
    });
  }

  function handleSubmit(event) {
    event.preventDefault();
    console.log(form);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="email"    name="email"    value={form.email}    onChange={handleChange} placeholder="Email" />
      <input type="password" name="password" value={form.password} onChange={handleChange} placeholder="Password" />
      <button type="submit">Login</button>
    </form>
  );
}

[event.target.name] is a computed property key — it uses the input's name attribute as the object key dynamically.

⚠️ Unlike class components, useState does NOT auto-merge — you must spread ...form manually or you'll lose all other fields.


Array State — List

Never use .push() directly on state arrays — it mutates the original without triggering a re-render. Always create a new array.

import { useState } from 'react';

function TodoList() {
  const [todos, setTodos] = useState(['Buy milk', 'Go for a walk']);
  const [input, setInput] = useState('');

  function addTodo() {
    if (!input) return;
    setTodos([...todos, input]); // spread existing + add new item
    setInput('');
  }

  function removeTodo(index) {
    setTodos(todos.filter((_, i) => i !== index));
  }

  return (
    <div>
      <input value={input} onChange={e => setInput(e.target.value)} placeholder="New task" />
      <button onClick={addTodo}>Add</button>
      <ul>
        {todos.map((todo, index) => (
          <li key={index}>
            {todo}
            <button onClick={() => removeTodo(index)}></button>
          </li>
        ))}
      </ul>
    </div>
  );
}

Functional Update Form

When new state depends on previous state, pass a function to the setter instead of a value. This guarantees you always work with the latest state.

// ❌ Can be stale in edge cases
setCount(count + 1);

// ✅ Always uses latest state
setCount(prevCount => prevCount + 1);

Practical example — two increments on one click:

function handleClick() {
  setCount(prev => prev + 1);
  setCount(prev => prev + 1); // correctly adds 2
}
// Using setCount(count + 1) twice would only add 1 — both reads are stale

Multiple State Variables

Each useState call is independent.

const [name, setName]         = useState('');
const [age, setAge]           = useState('');
const [isOnline, setIsOnline] = useState(false);

Rules of Hooks

  1. Only call Hooks at the top level — never inside if, loops, or nested functions.
  2. Only call Hooks inside functional components — not in plain JS functions.
// ❌ Wrong — inside an if
function App() {
  if (someCondition) {
    const [count, setCount] = useState(0); // not allowed
  }
}

// ✅ Correct — always at the top level
function App() {
  const [count, setCount] = useState(0);
  if (someCondition) { /* use count here */ }
}


Part 2 — Class Component State (this.state)

Before React 16.8, state could only be used in class components. Understanding this is essential for reading older codebases.


Defining State — this.state

State is defined inside the constructor as a plain object assigned to this.state.

import React from 'react';

class Counter extends React.Component {
  constructor(props) {
    super(props); // must always be called first

    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
      </div>
    );
  }
}

super(props) runs the parent React.Component constructor which sets up the component internally. It must be called before anything else.


Updating State — this.setState()

Never update state directly. Always use this.setState().

// ❌ Wrong — direct mutation, no re-render
this.state.count = this.state.count + 1;

// ✅ Correct — triggers re-render
this.setState({ count: this.state.count + 1 });

The this Binding Problem

When you pass a method to an event like onClick, JavaScript loses the this context. this becomes undefined inside the method and this.setState throws an error.

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
    // ❌ no binding — this will crash
  }

  handleClick() {
    this.setState({ count: this.state.count + 1 }); // 'this' is undefined!
  }

  render() {
    return <button onClick={this.handleClick}>Add</button>; // passes as plain function
  }
}

Why? When you write onClick={this.handleClick} you pass the function as a plain reference. When the click fires, it runs as a standalone function — not as instance.handleClick() — so this is undefined.


Fixing this — 3 Ways

Fix 1 — .bind(this) in Constructor ✅ Classic Standard

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };

    this.handleClick = this.handleClick.bind(this); // permanently locks 'this'
  }

  handleClick() {
    this.setState({ count: this.state.count + 1 }); // ✅ works
  }

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.handleClick}>Add</button>
      </div>
    );
  }
}

.bind(this) creates a new function with this permanently locked to the component instance.


Fix 2 — Inline Arrow Function in JSX ⚠️ Quick but less efficient

render() {
  return (
    <button onClick={() => this.handleClick()}>Add</button>
  );
}

Arrow functions inherit this from the surrounding scope, so it works. However, a new function is created on every render — fine for small apps, but avoid in performance-sensitive components.


Fix 3 — Class Field Arrow Function ✅ Modern Standard

Define the method as an arrow function directly as a class property. No constructor binding needed.

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  // arrow function as class field — 'this' always refers to the component
  handleClick = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.handleClick}>Add</button>
      </div>
    );
  }
}

Binding Comparison

Approach Where? New fn on render? Recommended?
.bind(this) in constructor Constructor ❌ No ✅ Classic
() => this.fn() in JSX JSX ✅ Yes ⚠️ Small apps only
Class field arrow fn = () => {} Method definition ❌ No ✅ Modern

setState Merges Automatically

When you call setState with a partial object, React auto-merges it — you do not need to spread manually.

this.state = { name: 'Arjun', age: 22, isOnline: false };

this.setState({ isOnline: true });
// Result: { name: 'Arjun', age: 22, isOnline: true } — name and age preserved ✅

This is the opposite of useState with an object, where you must manually spread ...prev.


Multiple State Properties

All state lives in one object in a class component.

class UserCard extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: '',
      age: '',
      isOnline: false
    };
  }

  render() {
    return (
      <div>
        <p>Name: {this.state.name}</p>
        <p>Age: {this.state.age}</p>
        <p>Status: {this.state.isOnline ? '🟢 Online' : '🔴 Offline'}</p>
      </div>
    );
  }
}

Functional Update in Class

When new state depends on old state, pass a function to setState.

// ❌ Can read stale state
this.setState({ count: this.state.count + 1 });

// ✅ Always reads latest state
this.setState(prevState => ({ count: prevState.count + 1 }));

Controlled Input in a Class Component

import React from 'react';

class NameInput extends React.Component {
  constructor(props) {
    super(props);
    this.state = { name: '' };
    this.handleChange = this.handleChange.bind(this); // ✅
  }

  handleChange(event) {
    this.setState({ name: event.target.value });
  }

  render() {
    return (
      <div>
        <input
          type="text"
          value={this.state.name}
          onChange={this.handleChange}
          placeholder="Enter your name"
        />
        <p>Hello, {this.state.name}!</p>
      </div>
    );
  }
}


Part 3 — Side-by-Side Comparison

Counter — Same Component, Both Ways

// ── FUNCTIONAL ──────────────────────────────
import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Add</button>
    </div>
  );
}


// ── CLASS ────────────────────────────────────
import React from 'react';

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState({ count: this.state.count + 1 });
  }

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.handleClick}>Add</button>
      </div>
    );
  }
}

Full Differences Table

Feature Functional (useState) Class (this.state)
State declaration const [x, setX] = useState(val) this.state = { x: val } in constructor
Reading state x this.state.x
Updating state setX(newVal) this.setState({ x: newVal })
Multiple state Separate useState calls One this.state object
Object update Must spread { ...prev, key: val } Auto-merges, no spread needed
Array update Must create new array (no .push) Must create new array (no .push)
Previous state setX(prev => prev + 1) this.setState(prev => ...)
Binding methods ❌ Not needed ✅ Required — .bind(this) or arrow fn
this keyword ❌ Never used ✅ Required everywhere
Needs constructor ❌ No ✅ Yes
Modern standard ✅ Yes — preferred ❌ Legacy — avoid for new code

Quick Reference

Functional

Goal Code
Declare state const [val, setVal] = useState(initial)
Update state setVal(newValue)
Update from previous setVal(prev => prev + 1)
Update object setVal({ ...val, key: newValue })
Add to array setVal([...val, newItem])
Remove from array setVal(val.filter(item => item.id !== id))
Toggle boolean setVal(!val)
Bind to input value={val} onChange={e => setVal(e.target.value)}

Class

Goal Code
Define state this.state = { key: value } inside constructor
Read state this.state.key
Update state this.setState({ key: newValue })
Update from previous this.setState(prev => ({ key: prev.key + 1 }))
Bind method (classic) this.fn = this.fn.bind(this) in constructor
Bind method (modern) Define as fn = () => { } class field
Always first in constructor super(props)

Part of the React Project Wiki — see also: [React Components](./react-components.md) · [Fragments & Event Handling](./fragments-event-handling.md)

⚠️ **GitHub.com Fallback** ⚠️