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 |
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.
useState is a React Hook that creates a state variable. When you update it using the setter function, React automatically re-renders the component.
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 destructuring — useState returns an array of two items and we name them whatever we want.
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:
- Component renders —
countis0 - User clicks —
handleClickruns -
setCount(count + 1)is called — state updates to1 - React re-renders — screen shows
Count: 1
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: '' });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
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.
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,useStatedoes NOT auto-merge — you must spread...formmanually or you'll lose all other fields.
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>
);
}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 staleEach useState call is independent.
const [name, setName] = useState('');
const [age, setAge] = useState('');
const [isOnline, setIsOnline] = useState(false);-
Only call Hooks at the top level — never inside
if, loops, or nested functions. - 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 */ }
}Before React 16.8, state could only be used in class components. Understanding this is essential for reading older codebases.
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.
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 });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.
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.
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.
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>
);
}
}| Approach | Where? | New fn on render? | Recommended? |
|---|---|---|---|
.bind(this) in constructor |
Constructor | ❌ No | ✅ Classic |
() => this.fn() in JSX |
JSX | ✅ Yes | |
Class field arrow fn = () => {}
|
Method definition | ❌ No | ✅ Modern |
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
useStatewith an object, where you must manually spread...prev.
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>
);
}
}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 }));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>
);
}
}// ── 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>
);
}
}| 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 |
| 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)} |
| 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)