Skip to main content
Props vs State

State & useState Hook

State is what makes React components interactive. It’s data that changes over time and triggers re-renders when updated.

State vs Props

Understanding the difference is crucial:
PropsState
Passed from parentManaged within component
Read-only (immutable)Can be updated
Received as function parametersCreated with useState
Changes come from parentChanges come from component itself
┌─────────────────────────────────────────────────────────────────┐
│                        Parent Component                         │
│                                                                 │
│                    ┌─────────────────────┐                     │
│                    │  state = { count }  │                     │
│                    └─────────────────────┘                     │
│                              │                                  │
│                    passes as prop                               │
│                              ▼                                  │
│              ┌───────────────────────────────┐                 │
│              │      Child Component          │                 │
│              │   props = { count: 5 }        │                 │
│              │   (read-only)                 │                 │
│              └───────────────────────────────┘                 │
└─────────────────────────────────────────────────────────────────┘

The useState Hook

Hooks are functions that let you “hook into” React features. useState is the fundamental hook for managing state.

Basic Syntax

import { useState } from 'react';

const [stateValue, setStateFunction] = useState(initialValue);
  • stateValue: Current value of the state
  • setStateFunction: Function to update the state
  • initialValue: Starting value (any type: string, number, boolean, object, array)

Simple Counter Example

import { useState } from 'react';

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

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

Toggle Example

function ToggleButton() {
  const [isOn, setIsOn] = useState(false);

  return (
    <button 
      onClick={() => setIsOn(!isOn)}
      style={{ backgroundColor: isOn ? 'green' : 'gray' }}
    >
      {isOn ? 'ON' : 'OFF'}
    </button>
  );
}

Rules of useState

1. Never Modify State Directly

React won’t know the state changed and won’t re-render.
// ❌ WRONG - Direct mutation
count = count + 1;

// ✅ CORRECT - Use the setter function
setCount(count + 1);

2. Call Hooks at the Top Level

Don’t call hooks inside loops, conditions, or nested functions.
function BadComponent() {
  // ❌ WRONG - Hook inside condition
  if (someCondition) {
    const [value, setValue] = useState(0);
  }
}

function GoodComponent() {
  // ✅ CORRECT - Hook at top level
  const [value, setValue] = useState(0);
  
  // Use conditions with the state value, not the hook
  if (someCondition) {
    // do something with value
  }
}

3. Only Call Hooks in React Functions

Hooks only work in React function components or custom hooks.

Functional Updates (Previous State)

When your new state depends on the previous state, use the functional update form:
// ❌ Can cause bugs with multiple rapid updates
setCount(count + 1);
setCount(count + 1); // Both use the same stale 'count'

// ✅ CORRECT - Uses the latest state
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1); // Each gets the updated value
Why does this matter? React batches state updates for performance. When you click a button that triggers multiple setCount calls, they may all read the same initial value of count.

Practical Example

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

  const handleClick = () => {
    // All three will use functional updates for correct behavior
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
    // Result: count increments by 3
  };

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

Lazy Initialization

If your initial state requires expensive computation, pass a function:
// ❌ Expensive function runs on EVERY render
const [data, setData] = useState(expensiveComputation());

// ✅ Function only runs on initial render
const [data, setData] = useState(() => expensiveComputation());

Real-World Example

function TodoApp() {
  // Load todos from localStorage only on first render
  const [todos, setTodos] = useState(() => {
    const saved = localStorage.getItem('todos');
    return saved ? JSON.parse(saved) : [];
  });

  // ...
}

State with Objects

When state is an object, you must spread the previous state to avoid losing properties.
function UserForm() {
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: 0
  });

  const updateName = (newName) => {
    // ❌ WRONG - loses email and age
    setUser({ name: newName });
    
    // ✅ CORRECT - spread previous state
    setUser({ ...user, name: newName });
    
    // ✅ ALSO CORRECT - functional update
    setUser(prev => ({ ...prev, name: newName }));
  };

  return (
    <form>
      <input
        value={user.name}
        onChange={(e) => setUser({ ...user, name: e.target.value })}
        placeholder="Name"
      />
      <input
        value={user.email}
        onChange={(e) => setUser({ ...user, email: e.target.value })}
        placeholder="Email"
      />
    </form>
  );
}

Nested Objects

For deeply nested objects, consider flattening your state or using a library like Immer.
const [user, setUser] = useState({
  name: 'Alice',
  address: {
    city: 'NYC',
    zip: '10001'
  }
});

// Updating nested property
setUser(prev => ({
  ...prev,
  address: {
    ...prev.address,
    city: 'LA'
  }
}));

State with Arrays

Arrays require special handling because they’re reference types.
function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React', done: false },
    { id: 2, text: 'Build project', done: false }
  ]);

  // ✅ Add item
  const addTodo = (text) => {
    setTodos([...todos, { id: Date.now(), text, done: false }]);
  };

  // ✅ Remove item
  const removeTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  // ✅ Update item
  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, done: !todo.done } : todo
    ));
  };

  // ✅ Insert at specific position
  const insertAt = (index, newTodo) => {
    setTodos([
      ...todos.slice(0, index),
      newTodo,
      ...todos.slice(index)
    ]);
  };

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <input
            type="checkbox"
            checked={todo.done}
            onChange={() => toggleTodo(todo.id)}
          />
          <span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
            {todo.text}
          </span>
          <button onClick={() => removeTodo(todo.id)}>Delete</button>
        </li>
      ))}
    </ul>
  );
}
Never mutate arrays directly! Methods like push, pop, splice, sort, and reverse mutate the original array.
// ❌ WRONG - mutates original array
todos.push(newTodo);
setTodos(todos);

// ✅ CORRECT - creates new array
setTodos([...todos, newTodo]);

Array Operation Cheat Sheet

OperationMutating (Don’t Use)Non-Mutating (Use This)
Addpush, unshift[...arr, item], [item, ...arr]
Removepop, shift, splicefilter()
Replacearr[i] = x, splicemap()
Sortsort()[...arr].sort()
Reversereverse()[...arr].reverse()

Multiple State Variables

You can (and often should) use multiple useState calls:
function UserProfile() {
  // Separate concerns into individual state pieces
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [isSubscribed, setIsSubscribed] = useState(false);
  const [notifications, setNotifications] = useState([]);
  
  // Each can be updated independently
}

When to Use Object State vs Multiple States

// ✅ Multiple states - when values change independently
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');

// ✅ Object state - when values always change together
const [position, setPosition] = useState({ x: 0, y: 0 });

// ✅ Object state - for forms with many fields
const [formData, setFormData] = useState({
  username: '',
  email: '',
  password: '',
  confirmPassword: ''
});

State Updates and Re-rendering

When state changes, React:
  1. Schedules a re-render
  2. Calls your component function again
  3. Compares the new JSX with the previous
  4. Updates only what changed in the DOM
function RenderDemo() {
  const [count, setCount] = useState(0);
  
  console.log('Component rendered!'); // Logs on every render

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}
State updates are asynchronous. The new value isn’t immediately available after calling the setter.
const [count, setCount] = useState(0);

const handleClick = () => {
  setCount(count + 1);
  console.log(count); // Still logs 0, not 1!
};

🎯 Practice Exercises

Build a product card with quantity controls:
function ProductCard({ name, price }) {
  const [quantity, setQuantity] = useState(0);

  const increment = () => setQuantity(prev => prev + 1);
  const decrement = () => setQuantity(prev => Math.max(0, prev - 1));

  return (
    <div className="product-card">
      <h3>{name}</h3>
      <p>${price}</p>
      <div className="quantity-controls">
        <button onClick={decrement} disabled={quantity === 0}></button>
        <span>{quantity}</span>
        <button onClick={increment}>+</button>
      </div>
      <p>Total: ${(price * quantity).toFixed(2)}</p>
    </div>
  );
}
Build an FAQ item that expands/collapses:
function FAQItem({ question, answer }) {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div className="faq-item">
      <button 
        onClick={() => setIsOpen(!isOpen)}
        className="faq-question"
      >
        {question}
        <span>{isOpen ? '−' : '+'}</span>
      </button>
      {isOpen && (
        <div className="faq-answer">
          {answer}
        </div>
      )}
    </div>
  );
}

// Usage
<FAQItem 
  question="What is React?" 
  answer="React is a JavaScript library for building user interfaces."
/>
Build a form that tracks data across multiple steps:
function MultiStepForm() {
  const [step, setStep] = useState(1);
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    plan: 'basic'
  });

  const updateField = (field, value) => {
    setFormData(prev => ({ ...prev, [field]: value }));
  };

  const nextStep = () => setStep(prev => Math.min(prev + 1, 3));
  const prevStep = () => setStep(prev => Math.max(prev - 1, 1));

  return (
    <div className="multi-step-form">
      <div className="progress">Step {step} of 3</div>
      
      {step === 1 && (
        <div>
          <h2>Personal Info</h2>
          <input
            value={formData.name}
            onChange={(e) => updateField('name', e.target.value)}
            placeholder="Your name"
          />
        </div>
      )}
      
      {step === 2 && (
        <div>
          <h2>Contact</h2>
          <input
            type="email"
            value={formData.email}
            onChange={(e) => updateField('email', e.target.value)}
            placeholder="Your email"
          />
        </div>
      )}
      
      {step === 3 && (
        <div>
          <h2>Review</h2>
          <p>Name: {formData.name}</p>
          <p>Email: {formData.email}</p>
          <p>Plan: {formData.plan}</p>
        </div>
      )}
      
      <div className="buttons">
        {step > 1 && <button onClick={prevStep}>Back</button>}
        {step < 3 ? (
          <button onClick={nextStep}>Next</button>
        ) : (
          <button onClick={() => alert('Submitted!')}>Submit</button>
        )}
      </div>
    </div>
  );
}

Summary

ConceptDescription
useStateHook to add state to functional components
Setter FunctionOnly way to update state and trigger re-render
Functional UpdatessetState(prev => newValue) for updates based on previous state
Lazy InitializationuseState(() => expensiveCalc()) for expensive initial values
ImmutabilityNever mutate state directly; always create new values
ObjectsSpread previous state: { ...prev, newProp }
ArraysUse map, filter, spread; never push, pop, splice
BatchingReact batches multiple state updates for performance

Next Steps

In the next chapter, you’ll learn about Handling Events — responding to user clicks, form submissions, and keyboard input!