React 15 ⚛️ Introduction to State and useState
State is the data a component owns and can change over time. Props are read-only and come from the parent; state is mutable and belongs to the component itself. When a component’s state changes, React re-renders the component with the new values, and the UI reflects the change. This is the mechanism that makes React applications interactive: a user types in an input, clicks a button, or receives data from a server, and the state updates, which triggers a re-render.
Before hooks, state existed only in class components, accessed through this.state and updated with this.setState. The useState hook, introduced in React 16.8, brought state to function components. It returns a pair: the current value and a setter function. Calling the setter updates the value and schedules a re-render. The hook is the most fundamental of React’s hooks, and understanding it is the foundation for understanding the rest.
This chapter covers what state is, the useState hook and its return value, how re-renders work, the difference between state and props, functional updates, batching, initial state and lazy initialization, state with objects and arrays, and the patterns that prevent the most common mistakes.
Key point: useState returns a pair: the current value and a setter function. Calling the setter updates the state and triggers a re-render. State updates are asynchronous and batched; use functional updates (setCount(c => c + 1)) when the new value depends on the old. Never mutate state directly — always replace it with a new value.
Why state exists
The interactivity problem. A static component renders the same output every time. An interactive component needs to change in response to user actions: a counter increments, a form fills in, a list filters. Without state, there is nowhere to store the changing values, and React has no reason to re-render.
The ownership problem. Props come from the parent and cannot be changed by the child. Some data must be owned by the component that renders it: the open/closed status of a dropdown, the current value of an input before submission, the loading state of a fetch. State is the place for data that a component owns and controls.
The re-render problem. React re-renders a component when its state or props change. This is how the UI stays in sync with the data. The setter function returned by useState is the trigger: it updates the state and tells React to re-render the component with the new value.
The simplicity problem. Before useState, state required a class, a constructor, this.state, and this.setState. The hook reduces this to a single function call and removes the this binding problem. Function components with hooks became the standard because they are simpler to write and to read.
The encapsulation problem. Each useState call manages one piece of state. A component with three independent values uses three useState calls, each with its own setter. This keeps the state shape simple and the updates predictable.
a. What state is
State is a component’s memory. It persists between renders and is owned by the component instance. When state changes, React re-renders the component.
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
The useState(0) call initializes count to 0. The setter setCount updates it. When setCount is called, React re-renders the component, and count holds the new value.
The initial value is used only on the first render. On subsequent renders, useState returns the current state, ignoring the argument.
const [count, setCount] = useState(0); // 0 only on first render
State is local to the component. Two instances of the same component have independent state.
<Counter /> // has its own count
<Counter /> // has its own count
Each instance calls useState and gets its own state variable. They do not share.
b. The useState hook
useState takes an initial value and returns an array with two elements: the current value and a setter function.
const [value, setValue] = useState(initialValue);
The array destructuring syntax names the two elements. The names are conventions, not requirements: [count, setCount], [name, setName], [isOpen, setIsOpen]. The setter is conventionally named set followed by the capitalized state name.
The initial value can be a primitive, an object, an array, or the result of a function.
const [count, setCount] = useState(0);
const [name, setName] = useState('Guest');
const [user, setUser] = useState(null);
const [items, setItems] = useState([]);
The setter accepts the new value:
setCount(5);
setName('Alice');
setUser({ id: 1, name: 'Bob' });
setItems([1, 2, 3]);
Calling the setter with the same value as the current state does not necessarily trigger a re-render. React may skip the re-render if the value has not changed by its comparison rules.
A component can have multiple state variables:
function Form() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState(0);
// ...
}
Each has its own setter and its own update cycle. Updating one does not affect the others.
c. Re-renders
When the setter is called, React schedules a re-render of the component. The re-render is not immediate; it happens after the current event handler completes and React has batched any other updates.
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
console.log(count); // still the old value
};
return <button onClick={handleClick}>{count}</button>;
}
The console.log(count) prints the old value because the state update is asynchronous. The variable count in the current render still holds the value from when the render happened. The new value is available only in the next render.
This is the most common source of confusion with useState. The state variable is a constant within a single render, and reading it immediately after calling the setter gives the old value.
The correct way to read the new value is in the next render, or by computing it before calling the setter:
const handleClick = () => {
const next = count + 1;
setCount(next);
console.log(next); // the new value
};
Or by using a functional update, which receives the current value as an argument:
setCount((prev) => prev + 1);
d. State versus props
State and props are both data that affect rendering, but they differ in ownership and mutability.
| Aspect | Props | State |
|---|---|---|
| Owner | Parent component | The component itself |
| Mutability | Read-only | Mutable via setter |
| Change triggers | Parent re-renders | Setter called |
| Source | Passed in JSX | Declared with useState |
| Persistence | Per parent render | Between renders |
A component cannot change its props. A component can change its state. When a component needs to change data, either the data is its own state, or the parent passes a callback that changes the parent’s state.
The rule of thumb: state should live in the component that needs to change it. If two components need to share the same changing data, the state moves to their closest common ancestor, and the data flows down as props while callbacks flow up.
Lifting state up is the pattern that moves state from a child to a parent so that siblings can share it.
function Parent() {
const [value, setValue] = useState('');
return (
<>
<Input value={value} onChange={setValue} />
<Preview value={value} />
</>
);
}
The parent owns the state. The input receives the value and a setter, and the preview receives the value. Both stay in sync.
e. Functional updates
When the new state depends on the old state, the setter should receive a function rather than a value. The function receives the previous state and returns the new state.
setCount((prev) => prev + 1);
The functional form is necessary when the update depends on the previous value and the setter is called multiple times in a row, or when the setter is called from an async callback.
// Without functional update: both calls use the same count
const handleClick = () => {
setCount(count + 1);
setCount(count + 1); // both use the same old count
// count increases by 1, not 2
};
// With functional update: each call receives the latest value
const handleClick = () => {
setCount((c) => c + 1);
setCount((c) => c + 1);
// count increases by 2
};
React batches the two updates, but the functional form ensures that each receives the result of the previous one. The non-functional form captures the count from the current render, which is the same for both calls.
The same applies to state updates inside setTimeout, promises, or event handlers that fire after the render has finished:
setTimeout(() => {
setCount((c) => c + 1); // always current
}, 1000);
f. Batching
React batches multiple state updates into a single re-render. When several setters are called in the same event handler, React performs one re-render rather than one per setter.
const handleClick = () => {
setName('Alice');
setAge(30);
setCity('Oslo');
// One re-render, not three
};
Batching is automatic in React 18 and later, including in promises, timeouts, and native event handlers. In React 17 and earlier, batching applied only to updates inside React event handlers.
The batching means that a handler can update multiple pieces of state, and the component re-renders once with all the new values. This improves performance and ensures that intermediate states are not rendered.
g. Initial state and lazy initialization
The initial value passed to useState is used only on the first render. If the initial value is expensive to compute, pass a function instead of a value. React calls the function only on the first render.
// Expensive computation runs on every render
const [data, setData] = useState(computeExpensiveData());
// Expensive computation runs only on first render
const [data, setData] = useState(() => computeExpensiveData());
The lazy form is a function that returns the initial value. React calls it once, on the first render, and uses the result as the initial state. Subsequent renders ignore the function.
This matters when the computation is expensive, such as parsing a large JSON string, reading from localStorage, or building a complex data structure.
const [user, setUser] = useState(() => {
const stored = localStorage.getItem('user');
return stored ? JSON.parse(stored) : null;
});
h. State with objects and arrays
State can hold any JavaScript value, including objects and arrays. The rule for updating them is to replace rather than mutate.
// Wrong: mutates the existing object
user.name = 'Alice';
setUser(user);
// Correct: creates a new object
setUser({ ...user, name: 'Alice' });
React compares the new state to the old by reference. If the reference is the same, React may skip the re-render. Mutating the object and passing it back does not change the reference, so React does not see a change.
The same applies to arrays:
// Wrong: mutates the array
items.push(newItem);
setItems(items);
// Correct: creates a new array
setItems([...items, newItem]);
Updating a nested object requires spreading at each level:
setUser({
...user,
address: { ...user.address, city: 'Oslo' },
});
The spread operator creates new objects at each level, changing the reference and triggering the re-render.
For arrays, the common operations map to non-mutating equivalents:
| Operation | Mutating | Non-mutating |
|---|---|---|
| Add | push | [...arr, item] |
| Remove | splice | arr.filter(x => x !== item) |
| Update | arr[i] = x | arr.map((x, j) => j === i ? newX : x) |
| Insert | splice | [...arr.slice(0, i), item, ...arr.slice(i)] |
Each produces a new array, changing the reference and triggering a re-render.
Complete Example Session
// ============================================
// PART 1: BASIC COUNTER
// ============================================
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
// ============================================
// PART 2: STATE IS ASYNCHRONOUS
// ============================================
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
console.log(count); // still the old value
};
return <button onClick={handleClick}>{count}</button>;
}
// ============================================
// PART 3: FUNCTIONAL UPDATE
// ============================================
function Counter() {
const [count, setCount] = useState(0);
const incrementTwice = () => {
setCount((c) => c + 1);
setCount((c) => c + 1);
};
return (
<div>
<p>{count}</p>
<button onClick={incrementTwice}>+2</button>
</div>
);
}
// ============================================
// PART 4: MULTIPLE STATE VARIABLES
// ============================================
function Form() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState(0);
return (
<form>
<input value={name} onChange={(e) => setName(e.target.value)} />
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<input type="number" value={age}
onChange={(e) => setAge(Number(e.target.value))} />
</form>
);
}
// ============================================
// PART 5: STATE WITH OBJECT
// ============================================
function Profile() {
const [user, setUser] = useState({ name: 'Alice', age: 30 });
const updateName = (newName) => {
setUser({ ...user, name: newName });
};
return (
<div>
<p>{user.name}, {user.age}</p>
<button onClick={() => updateName('Bob')}>Change name</button>
</div>
);
}
// ============================================
// PART 6: STATE WITH ARRAY — ADD
// ============================================
function TodoList() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
const addTodo = () => {
setTodos([...todos, { id: Date.now(), text: input }]);
setInput('');
};
return (
<div>
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button onClick={addTodo}>Add</button>
<ul>
{todos.map((t) => <li key={t.id}>{t.text}</li>)}
</ul>
</div>
);
}
// ============================================
// PART 7: STATE WITH ARRAY — REMOVE
// ============================================
const removeTodo = (id) => {
setTodos(todos.filter((t) => t.id !== id));
};
// ============================================
// PART 8: LAZY INITIALIZATION
// ============================================
const [data, setData] = useState(() => {
const stored = localStorage.getItem('data');
return stored ? JSON.parse(stored) : [];
});
// ============================================
// PART 9: BATCHING
// ============================================
const handleClick = () => {
setName('Alice');
setAge(30);
setCity('Oslo');
// One re-render with all three values updated
};
// ============================================
// PART 10: TOGGLE STATE
// ============================================
function Toggle() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen((v) => !v)}>
{isOpen ? 'Close' : 'Open'}
</button>
{isOpen && <p>Content is visible</p>}
</div>
);
}
These ten parts cover the basic counter, asynchronous state, functional updates, multiple state variables, objects, arrays with add and remove, lazy initialization, batching, and toggle state.
Quick Reference
useState Syntax
| Form | Example |
|---|---|
| Primitive | const [count, setCount] = useState(0) |
| String | const [name, setName] = useState('') |
| Boolean | const [isOpen, setIsOpen] = useState(false) |
| Object | const [user, setUser] = useState({}) |
| Array | const [items, setItems] = useState([]) |
| Null | const [data, setData] = useState(null) |
| Lazy | useState(() => expensive()) |
Update Patterns
| Pattern | Example |
|---|---|
| Direct value | setCount(5) |
| Functional | setCount((c) => c + 1) |
| Object spread | setUser({ ...user, name: 'Alice' }) |
| Array add | setItems([...items, newItem]) |
| Array remove | setItems(items.filter((i) => i.id !== id)) |
| Array update | setItems(items.map((i) => i.id === id ? newItem : i)) |
| Toggle | setIsOpen((v) => !v) |
State vs Props
| Aspect | State | Props |
|---|---|---|
| Owned by | Component | Parent |
| Mutable | Yes (via setter) | No |
| Initial value | useState(arg) | From JSX |
| Persists | Between renders | Per render |
| Change triggers | Setter call | Parent re-render |
Common Operations
| Operation | Pattern |
|---|---|
| Increment | setCount((c) => c + 1) |
| Reset | setCount(0) |
| Add to list | setItems([...items, item]) |
| Remove from list | setItems(items.filter(...)) |
| Update in list | setItems(items.map(...)) |
| Update object | setUser({ ...user, key: value }) |
Best Practices
✅ Do This:
// Use functional update when depending on previous state
setCount((c) => c + 1);
// Spread to update objects and arrays
setUser({ ...user, name: 'Alice' });
setItems([...items, newItem]);
// Use lazy initialization for expensive initial values
useState(() => computeExpensive());
// Keep state minimal and focused
const [isOpen, setIsOpen] = useState(false);
❌ Don’t Do This:
// Mutate state directly
user.name = 'Alice'; // ❌
items.push(newItem); // ❌
// Rely on state immediately after setting
setCount(count + 1);
console.log(count); // ❌ old value
// Use state for derived values
const [fullName, setFullName] = useState(first + ' ' + last); // ❌ compute it
// Store props in state
const [name, setName] = useState(props.name); // ❌ usually wrong
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| State not updating | Mutated instead of replaced | Create a new object or array |
| Reading old value | State update is async | Read in next render or compute first |
| Multiple updates collapse | Used direct value form | Use functional update |
| Re-render on same value | Unnecessary state change | Check value before setting |
| Expensive initial value | Called on every render | Use lazy initialization |
| Stale state in callback | Closure captured old value | Use functional update |
Real-World Examples
1. Counter
const [count, setCount] = useState(0);
<button onClick={() => setCount((c) => c + 1)}>+</button>
2. Controlled Input
const [value, setValue] = useState('');
<input value={value} onChange={(e) => setValue(e.target.value)} />
3. Toggle
const [isOpen, setIsOpen] = useState(false);
<button onClick={() => setIsOpen((v) => !v)}>Toggle</button>
4. Todo List
const [todos, setTodos] = useState([]);
setTodos((prev) => [...prev, newTodo]);
setTodos((prev) => prev.filter((t) => t.id !== id));
5. Form Object
const [form, setForm] = useState({ name: '', email: '' });
setForm((prev) => ({ ...prev, [field]: value }));
6. Loading State
const [isLoading, setIsLoading] = useState(false);
7. Error State
const [error, setError] = useState(null);
8. Selected Item
const [selectedId, setSelectedId] = useState(null);
9. Lazy Local Storage
const [theme, setTheme] = useState(() =>
localStorage.getItem('theme') ?? 'light'
);
10. Reset State
const reset = () => {
setCount(0);
setName('');
setItems([]);
};
Visual
State Update Flow
┌──────────────────────────────────────────────────────────────┐
│ 1. Component renders with count = 0 │
│ │ │
│ 2. User clicks button │
│ │ │
│ 3. Handler calls setCount(1) │
│ │ │
│ 4. React schedules re-render │
│ │ │
│ 5. Handler completes │
│ │ │
│ 6. React re-renders component with count = 1 │
│ │ │
│ 7. UI updates │
└──────────────────────────────────────────────────────────────┘
State vs Props
┌──────────────────────────────────────────────────────────────┐
│ PROPS STATE │
│ ┌───────────────────────────┐ ┌───────────────────────────┐│
│ │ Read-only │ │ Read/write via setter ││
│ │ From parent │ │ Owned by component ││
│ │ Changes when parent │ │ Changes via setter ││
│ │ re-renders │ │ ││
│ └───────────────────────────┘ └───────────────────────────┘│
│ │
│ Props go down. State changes internally. │
│ To change shared state, lift it to the common ancestor. │
└──────────────────────────────────────────────────────────────┘
Functional vs Direct Update
┌──────────────────────────────────────────────────────────────┐
│ DIRECT UPDATE: │
│ setCount(count + 1); │
│ setCount(count + 1); │
│ └── Both use the same count from this render │
│ └── Result: count increases by 1 │
│ │
│ FUNCTIONAL UPDATE: │
│ setCount((c) => c + 1); │
│ setCount((c) => c + 1); │
│ └── Each receives the latest value │
│ └── Result: count increases by 2 │
└──────────────────────────────────────────────────────────────┘
Object and Array Updates
┌──────────────────────────────────────────────────────────────┐
│ OBJECT: │
│ setUser({ ...user, name: 'Alice' }) │
│ └── New object reference, triggers re-render │
│ │
│ ARRAY ADD: │
│ setItems([...items, newItem]) │
│ └── New array reference, triggers re-render │
│ │
│ ARRAY REMOVE: │
│ setItems(items.filter((i) => i.id !== id)) │
│ └── New array reference, triggers re-render │
│ │
│ ARRAY UPDATE: │
│ setItems(items.map((i) => i.id === id ? newItem : i)) │
│ └── New array reference, triggers re-render │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
useState | Hook for component state |
| Returns | [value, setter] pair |
| Initial value | Used only on first render |
| Lazy init | useState(() => expensive()) |
| Update | Call setter with new value |
| Functional update | setCount((c) => c + 1) |
| Asynchronous | State updates are not immediate |
| Batching | Multiple updates produce one re-render |
| Objects | Replace with spread, never mutate |
| Arrays | Use filter, map, spread |
| State vs props | State is owned, props are received |
| Lift state | Move to common ancestor to share |
Key takeaways:
useStatereturns a value and a setter. The value is the current state; the setter updates it and triggers a re-render. The initial value is used only on the first render.- State updates are asynchronous. Reading the state variable immediately after calling the setter gives the old value. The new value appears in the next render.
- Use functional updates when depending on the previous state.
setCount(c => c + 1)receives the latest value and is safe when called multiple times or from async callbacks. - Never mutate state directly. Objects and arrays must be replaced with new references. Spreading creates a new object or array, which React detects as a change.
- React batches multiple updates. Several setters called in the same handler produce a single re-render with all the new values.
- Lazy initialization avoids expensive recomputation. Passing a function to
useStateensures the initial value is computed only once. - State is local to the component. Two instances of the same component have independent state. To share state, lift it to the nearest common ancestor.
- State should be minimal. Values that can be computed from existing state or props should not be stored separately. Storing them creates the risk of them becoming out of sync.
Remember: State is the data a component owns and can change. useState is the hook that declares it, returning the current value and a setter. Calling the setter schedules a re-render, and the component renders again with the new value. The two most common mistakes are reading state immediately after setting it (the update is asynchronous) and mutating state instead of replacing it (React compares by reference). Functional updates solve the first; spreading solves the second. React batches multiple updates into one render, which keeps the component efficient. Lazy initialization avoids expensive work on every render. Understanding useState is understanding how React components become interactive, and it is the foundation for every other hook.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!