useState Hook in Functional Components

Understanding React's state management in functional components

Introduction to React State

State is one of the core concepts in React that allows components to create and manage their own data. Unlike props, which are passed down from parent components, state is internal to a component and can be updated over time.

Before React 16.8, state could only be used in class components. With the introduction of Hooks, functional components can now use state and other React features without writing a class.

The useState Hook

useState is a Hook that allows you to add React state to functional components. It's the most basic Hook in React and serves as the foundation for state management in function components.

flowchart LR A[useState Hook] --> B[Initial State] A --> C[State Variable] A --> D[State Setter Function] style A fill:#f9f,stroke:#333,stroke-width:2px

Anatomy of useState


// Basic syntax
const [state, setState] = useState(initialState);
      

The useState Hook returns a pair:

Real-world analogy: Think of useState like a hotel room safe. You put something in (initial state), you get a key (state variable) to see what's inside, and a passcode (setter function) that allows you to change the contents. Only you have access to this safe, and you can update its contents whenever you need to.

Basic Example: Counter

Let's start with a simple counter example to understand how useState works:


import React, { useState } from 'react';

function Counter() {
  // Declare a state variable named "count" with initial value of 0
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
      

In this example:

Behind the scenes: React remembers the current value of count between renders and provides the most up-to-date value to our function. When we call setCount, React re-renders the Counter component, passing the new count value to it.

Working with Different Data Types

useState can work with any data type - not just numbers, but also strings, booleans, arrays, objects, or any combination of these.

String Example: Form Input


function NameForm() {
  const [name, setName] = useState('');
  
  return (
    <form>
      <input 
        type="text" 
        value={name} 
        onChange={(e) => setName(e.target.value)} 
        placeholder="Enter your name"
      />
      <p>Hello, {name || 'stranger'}!</p>
    </form>
  );
}
      

Object Example: User Profile


function UserProfile() {
  const [user, setUser] = useState({
    name: '',
    email: '',
    bio: ''
  });
  
  const handleChange = (e) => {
    const { name, value } = e.target;
    setUser(prevUser => ({
      ...prevUser,      // Keep all other user properties
      [name]: value     // Update only the changed field
    }));
  };
  
  return (
    <form>
      <input 
        name="name"
        value={user.name} 
        onChange={handleChange} 
        placeholder="Name"
      />
      <input 
        name="email"
        value={user.email} 
        onChange={handleChange} 
        placeholder="Email"
      />
      <textarea 
        name="bio"
        value={user.bio} 
        onChange={handleChange} 
        placeholder="Bio"
      />
    </form>
  );
}
      

Important: When using objects or arrays in state, always treat state as immutable. Never directly modify the state object - instead, create a new copy with the changes you want.

State Updates and Re-Rendering

sequenceDiagram participant Component participant React participant DOM Component->>React: useState(initialValue) React-->>Component: [value, setValue] Component->>DOM: Render with current value Note over Component,DOM: Event occurs (e.g., click) Component->>React: setValue(newValue) React->>React: Schedule update React->>Component: Re-render with newValue Component->>DOM: Update DOM with new content

When you call the state setter function, React:

  1. Updates the internal state value
  2. Triggers a re-render of the component
  3. Provides the new state value to your component

Functional Updates

If the new state depends on the previous state, it's recommended to use the functional update form:


// Instead of this:
setCount(count + 1);

// Do this:
setCount(prevCount => prevCount + 1);
      

Why? React state updates may be batched for performance. Using the functional form ensures you're always working with the most current state value, even if multiple updates are queued.

Real-world example: Like counter in social media


function LikeButton() {
  const [likes, setLikes] = useState(0);
  const [isLiked, setIsLiked] = useState(false);
  
  const handleLike = () => {
    if (isLiked) {
      setLikes(prevLikes => prevLikes - 1);
      setIsLiked(false);
    } else {
      setLikes(prevLikes => prevLikes + 1);
      setIsLiked(true);
    }
  };
  
  return (
    <div>
      <button 
        onClick={handleLike}
        style={{ color: isLiked ? 'red' : 'gray' }}
      >
        {isLiked ? '❤️' : '🤍'} Like
      </button>
      <span>{likes} likes</span>
    </div>
  );
}
      

Multiple State Variables

You can use multiple useState Hooks in a single component to manage different pieces of state separately.


function UserForm() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [error, setError] = useState(null);
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsSubmitting(true);
    setError(null);
    
    try {
      // API call would go here
      await new Promise(resolve => setTimeout(resolve, 1000));
      alert(`Submitted: ${username}`);
      setUsername('');
      setPassword('');
    } catch (err) {
      setError('Submission failed');
    } finally {
      setIsSubmitting(false);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      {error && <p style={{ color: 'red' }}>{error}</p>}
      
      <div>
        <label>
          Username:
          <input 
            value={username} 
            onChange={e => setUsername(e.target.value)} 
            disabled={isSubmitting}
          />
        </label>
      </div>
      
      <div>
        <label>
          Password:
          <input 
            type="password"
            value={password} 
            onChange={e => setPassword(e.target.value)} 
            disabled={isSubmitting}
          />
        </label>
      </div>
      
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Submitting...' : 'Submit'}
      </button>
    </form>
  );
}
      

Decision point: When deciding whether to use multiple state variables or an object, consider:

Lazy Initial State

If your initial state is the result of an expensive computation, you can provide a function to useState which will only be executed on the initial render.


// Instead of this:
const [state, setState] = useState(expensiveComputation());

// Do this:
const [state, setState] = useState(() => expensiveComputation());
      

Example: Loading saved preferences


function UserPreferences() {
  // This function only runs once on initial render
  const [preferences, setPreferences] = useState(() => {
    // Potentially expensive operation
    const savedPrefs = localStorage.getItem('userPreferences');
    return savedPrefs ? JSON.parse(savedPrefs) : { 
      theme: 'light',
      fontSize: 'medium',
      notifications: true 
    };
  });
  
  const updatePreference = (key, value) => {
    const newPreferences = { ...preferences, [key]: value };
    setPreferences(newPreferences);
    localStorage.setItem('userPreferences', JSON.stringify(newPreferences));
  };
  
  return (
    <div>
      <h3>Your Preferences</h3>
      
      <div>
        <label>
          Theme:
          <select 
            value={preferences.theme} 
            onChange={e => updatePreference('theme', e.target.value)}
          >
            <option value="light">Light</option>
            <option value="dark">Dark</option>
          </select>
        </label>
      </div>
      
      <div>
        <label>
          Font Size:
          <select 
            value={preferences.fontSize} 
            onChange={e => updatePreference('fontSize', e.target.value)}
          >
            <option value="small">Small</option>
            <option value="medium">Medium</option>
            <option value="large">Large</option>
          </select>
        </label>
      </div>
      
      <div>
        <label>
          Notifications:
          <input 
            type="checkbox"
            checked={preferences.notifications}
            onChange={e => updatePreference('notifications', e.target.checked)}
          />
        </label>
      </div>
    </div>
  );
}
      

Common Pitfalls and Best Practices

State Updates are Asynchronous

React may batch multiple state updates for performance. Don't rely on previous state values for calculating the next state directly.

❌ Incorrect


function Counter() {
  const [count, setCount] = useState(0);
  
  const increment = () => {
    setCount(count + 1);
    setCount(count + 1); // Will not work as expected
  };
  
  return (
    <button onClick={increment}>
      Count: {count}
    </button>
  );
}
// Clicking will only increment by 1, not 2
          

✅ Correct


function Counter() {
  const [count, setCount] = useState(0);
  
  const increment = () => {
    setCount(prevCount => prevCount + 1);
    setCount(prevCount => prevCount + 1);
  };
  
  return (
    <button onClick={increment}>
      Count: {count}
    </button>
  );
}
// Clicking will increment by 2
          

State is Replaced, Not Merged

Unlike this.setState in class components, the state setter from useState doesn't automatically merge objects.

❌ Incorrect


function UserProfile() {
  const [user, setUser] = useState({
    name: 'John',
    email: 'john@example.com'
  });
  
  const updateEmail = (newEmail) => {
    // This will remove the name property!
    setUser({ email: newEmail });
  };
}
          

✅ Correct


function UserProfile() {
  const [user, setUser] = useState({
    name: 'John',
    email: 'john@example.com'
  });
  
  const updateEmail = (newEmail) => {
    // Spread the previous state and only update email
    setUser(prevUser => ({
      ...prevUser,
      email: newEmail
    }));
  };
}
          

Real-World Applications

Shopping Cart Component

A practical example of using useState for managing a shopping cart:


function ShoppingCart() {
  const [cart, setCart] = useState([]);
  const [total, setTotal] = useState(0);
  
  const addItem = (product) => {
    // Check if product already exists in cart
    const existingItemIndex = cart.findIndex(
      item => item.id === product.id
    );
    
    if (existingItemIndex >= 0) {
      // Product exists, increment quantity
      const updatedCart = [...cart];
      updatedCart[existingItemIndex] = {
        ...updatedCart[existingItemIndex],
        quantity: updatedCart[existingItemIndex].quantity + 1
      };
      setCart(updatedCart);
    } else {
      // Add new product with quantity 1
      setCart([...cart, { ...product, quantity: 1 }]);
    }
    
    // Update total
    setTotal(prevTotal => prevTotal + product.price);
  };
  
  const removeItem = (productId) => {
    const itemIndex = cart.findIndex(item => item.id === productId);
    if (itemIndex >= 0) {
      const item = cart[itemIndex];
      // Update total
      setTotal(prevTotal => prevTotal - (item.price * item.quantity));
      // Remove item from cart
      setCart(cart.filter(item => item.id !== productId));
    }
  };
  
  return (
    <div className="shopping-cart">
      <h2>Your Cart ({cart.length} items)</h2>
      
      {cart.length === 0 ? (
        <p>Your cart is empty</p>
      ) : (
        <ul>
          {cart.map(item => (
            <li key={item.id}>
              {item.name} - ${item.price} x {item.quantity}
              <button onClick={() => removeItem(item.id)}>Remove</button>
            </li>
          ))}
        </ul>
      )}
      
      <div className="cart-total">
        <strong>Total: ${total.toFixed(2)}</strong>
      </div>
      
      {/* Product list and add buttons would go here */}
    </div>
  );
}
      

Toggle Theme Feature

A common UI feature implementing dark/light mode:


function ThemeToggle() {
  // Check if user has a saved preference
  const [theme, setTheme] = useState(() => {
    const savedTheme = localStorage.getItem('theme');
    return savedTheme || 'light';
  });
  
  const toggleTheme = () => {
    const newTheme = theme === 'light' ? 'dark' : 'light';
    setTheme(newTheme);
    localStorage.setItem('theme', newTheme);
    // Also apply theme to document
    document.body.className = newTheme;
  };
  
  // Apply theme on initial render
  React.useEffect(() => {
    document.body.className = theme;
  }, [theme]);
  
  return (
    <button onClick={toggleTheme}>
      Switch to {theme === 'light' ? 'Dark' : 'Light'} Mode
    </button>
  );
}
      

When to Use useState vs Other State Management

useState is perfect for:

Consider other options when:

graph TD A[State Management Decision] --> B{Is state local to component?} B -->|Yes| C[useState] B -->|No| D{How complex is state logic?} D -->|Simple| E[Context API] D -->|Complex| F[Redux/MobX] C --> G{Complex state transitions?} G -->|Yes| H[useReducer] G -->|No| I[useState]

Practice Exercises

Exercise 1: Counter App

Create a counter app with increment, decrement, and reset buttons. Add a feature that prevents the counter from going below 0.

Exercise 2: Form with Validation

Build a form with at least three fields (name, email, password) and implement:

Exercise 3: Todo List

Create a todo list application with the following features:

Summary

In this lecture, we've learned:

The useState Hook is a fundamental building block for React applications, allowing you to create dynamic, interactive components without class syntax. Understanding how to use it effectively will make you more productive as a React developer.

Further Resources