State
intermediatePart of State & Props
Theory
State represents data that changes over time in a React component. Unlike props (which are read-only and passed from parents), state is internal, mutable, and managed within the component itself.
State vs Props
| Feature | Props | State | |---------|-------|-------| | Mutability | Immutable | Mutable | | Who manages it | Parent component | Component itself | | Purpose | Pass data down | Track changing data | | Triggers re-render | Yes | Yes |
The fundamental rule: props flow down, state changes trigger re-renders.
The useState Hook
useState is a hook — a special function that lets you add state to functional components:
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
}useState returns an array with two elements:
- The current state value (
count) - A setter function to update it (
setCount)
The argument passed to useState (here, 0) is the initial state.
Updating State
To update state, you must use the setter function — never modify state directly:
// WRONG - direct mutation
count = count + 1
// RIGHT - using the setter
setCount(count + 1)When the setter is called, React schedules a re-render of the component with the new state value.
For state updates that depend on the previous value, use the updater function pattern:
setCount(prevCount => prevCount + 1)This is important when updates happen in rapid succession or in closures.
State Immutability
State should always be treated as immutable. For objects and arrays, create a new copy instead of mutating:
// WRONG - mutating state
const [user, setUser] = useState({ name: 'Alice', age: 25 })
user.age = 26 // Direct mutation!
setUser(user) // React won't detect the change
// RIGHT - creating a new object
setUser({ ...user, age: 26 })For arrays:
const [items, setItems] = useState([1, 2, 3])
// Adding: create a new array with spread
setItems([...items, 4])
// Removing: filter to a new array
setItems(items.filter(item => item !== 2))
// Updating: map to a new array
setItems(items.map(item => item === 2 ? 99 : item))State in Forms (Controlled Components)
A controlled component is an input element whose value is controlled by React state:
function NameForm() {
const [name, setName] = useState('')
return (
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
)
}The input's value is bound to state, and the onChange handler updates state. This makes React the single source of truth for the input's value.
Lifting State Up
When multiple components need to share the same state, lift the state up to their closest common ancestor:
function Parent() {
const [value, setValue] = useState('')
return (
<>
<ChildA value={value} onChange={setValue} />
<ChildB value={value} />
</>
)
}This pattern keeps state predictable and makes data flow explicit.
State Management Patterns
For simple apps, local useState in individual components is sufficient. As your app grows, consider:
- Lifting state up — moving state to a parent component
- Context API — sharing state across many components without prop drilling
- useReducer — for complex state logic
- External libraries — Redux, Zustand, Jotai for global state
Start with local state. Only lift state up or add state management when you actually need it. Premature optimization adds unnecessary complexity.
Practical Examples
When state is duplicated across multiple components, it's a sign you need to lift it up. The state should live in the first common ancestor of the components that need it.
Exercises
Build a Like Button
Create a LikeButton component that tracks how many times it has been clicked. Display a heart icon (❤️) with the count. The button should toggle between liked and unliked states.
Expected Output:
A button showing ❤️ 0 initially. Each click increments the count. A second click on the same item decrements (toggle behavior).Shopping Cart Quantity
Create a QuantitySelector component that allows users to increase, decrease, and reset a quantity value. The quantity should never go below 0. Display the current quantity and three buttons: +, -, Reset.
Expected Output:
A counter that increments, decrements (but not below 0), and resets to 0Lift State Up: Shared Settings
Create a parent SettingsPage component that manages a theme ('light' | 'dark') and fontSize (number). Create two child components: ThemePicker and FontSizeSlider. Both children should read and update the shared state. The SettingsPage should display a preview paragraph with the current theme and font size applied.
Expected Output:
A settings page where changing the theme in ThemePicker updates the preview, and adjusting the slider in FontSizeSlider changes the font size of the previewMini Quiz
Mini Quiz
Mini Project
Mini Project: Interactive Quiz App
Build a quiz application where users can answer multiple-choice questions. The app tracks the current question index, the user's score, and highlights correct/incorrect answers.
Requirements:
Bonus Challenge
Add a timer for each question. If time runs out, automatically move to the next question.