Share:

State

intermediate

Part 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:

  1. The current state value (count)
  2. 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

Example 1: Todo List with State
jsx
Example 2: Lifting State Up
jsx

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

easy

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

medium

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 0

Lift State Up: Shared Settings

hard

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 preview

Mini 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.