Understanding State with useState Hook

Making Your React Components Dynamic and Interactive

Overview

Welcome to the world of state management! If props are like ingredients passed to a recipe, state is like the oven temperature that changes during cooking. Today, we'll learn how to use the useState Hook to add interactivity to our components, making them respond to user actions and update dynamically.

What is State?

State is data that changes over time within a component. Think of it like a chameleon that changes its color based on the environment - your component changes its appearance based on its state.

graph TD A[Component] --> B{Has State?} B -->|No| C[Static Component] B -->|Yes| D[Dynamic Component] D --> E[User Interaction] E --> F[State Changes] F --> G[Component Re-renders] G --> D style D fill:#f9f,stroke:#333,stroke-width:2px style F fill:#ff9,stroke:#333,stroke-width:2px

Props vs State

Understanding the difference between props and state is crucial:

Props State
Passed from parent Managed within component
Read-only Can be updated
Like function parameters Like local variables
Causes re-render when changed by parent Causes re-render when updated

Introducing useState Hook

The useState Hook is like a special container that holds a value and provides a way to update it:


import React, { useState } from 'react';

function Counter() {
    // useState returns an array: [currentValue, setterFunction]
    const [count, setCount] = useState(0);
    
    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={() => setCount(count + 1)}>
                Click me
            </button>
        </div>
    );
}
            

Anatomy of useState

graph LR A["const [state, setState] = useState(initialValue)"] A --> B[state] A --> C[setState] A --> D[initialValue] B --> E[Current value] C --> F[Function to update value] D --> G[Starting value] style A fill:#ffd,stroke:#333,stroke-width:2px

Basic useState Examples

1. Simple Counter


function Counter() {
    const [count, setCount] = useState(0);
    
    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={() => setCount(count + 1)}>Increment</button>
            <button onClick={() => setCount(count - 1)}>Decrement</button>
            <button onClick={() => setCount(0)}>Reset</button>
        </div>
    );
}
            

2. Text Input


function TextInput() {
    const [text, setText] = useState('');
    
    return (
        <div>
            <input
                type="text"
                value={text}
                onChange={(e) => setText(e.target.value)}
                placeholder="Type something..."
            />
            <p>You typed: {text}</p>
            <p>Character count: {text.length}</p>
        </div>
    );
}
            

3. Toggle Switch


function ToggleSwitch() {
    const [isOn, setIsOn] = useState(false);
    
    return (
        <div>
            <button onClick={() => setIsOn(!isOn)}>
                {isOn ? 'ON' : 'OFF'}
            </button>
            <p>The switch is {isOn ? 'on' : 'off'}</p>
        </div>
    );
}
            

Multiple State Variables

You can use useState multiple times in a single component:


function UserProfile() {
    const [name, setName] = useState('');
    const [age, setAge] = useState(0);
    const [email, setEmail] = useState('');
    
    return (
        <form>
            <input
                value={name}
                onChange={(e) => setName(e.target.value)}
                placeholder="Name"
            />
            <input
                type="number"
                value={age}
                onChange={(e) => setAge(parseInt(e.target.value))}
                placeholder="Age"
            />
            <input
                type="email"
                value={email}
                onChange={(e) => setEmail(e.target.value)}
                placeholder="Email"
            />
            
            <div>
                <h3>Profile Summary:</h3>
                <p>Name: {name}</p>
                <p>Age: {age}</p>
                <p>Email: {email}</p>
            </div>
        </form>
    );
}
            

Complex State with Objects

You can store objects in state, but you must be careful to update them immutably:


function ShoppingCart() {
    const [cart, setCart] = useState({
        items: [],
        total: 0,
        discount: 0
    });
    
    const addItem = (item) => {
        setCart(prevCart => ({
            ...prevCart,
            items: [...prevCart.items, item],
            total: prevCart.total + item.price
        }));
    };
    
    const applyDiscount = (amount) => {
        setCart(prevCart => ({
            ...prevCart,
            discount: amount
        }));
    };
    
    return (
        <div>
            <h2>Shopping Cart</h2>
            <p>Items: {cart.items.length}</p>
            <p>Total: ${cart.total}</p>
            <p>Discount: ${cart.discount}</p>
            <p>Final Price: ${cart.total - cart.discount}</p>
            
            <button onClick={() => addItem({ id: 1, name: 'Book', price: 20 })}>
                Add Book ($20)
            </button>
            <button onClick={() => applyDiscount(5)}>
                Apply $5 Discount
            </button>
        </div>
    );
}
            

State with Arrays

Managing arrays in state requires careful attention to immutability:


function TodoList() {
    const [todos, setTodos] = useState([]);
    const [inputValue, setInputValue] = useState('');
    
    const addTodo = () => {
        if (inputValue.trim()) {
            setTodos([...todos, {
                id: Date.now(),
                text: inputValue,
                completed: false
            }]);
            setInputValue('');
        }
    };
    
    const toggleTodo = (id) => {
        setTodos(todos.map(todo =>
            todo.id === id
                ? { ...todo, completed: !todo.completed }
                : todo
        ));
    };
    
    const removeTodo = (id) => {
        setTodos(todos.filter(todo => todo.id !== id));
    };
    
    return (
        <div>
            <input
                value={inputValue}
                onChange={(e) => setInputValue(e.target.value)}
                placeholder="Add a todo"
            />
            <button onClick={addTodo}>Add</button>
            
            <ul>
                {todos.map(todo => (
                    <li key={todo.id}>
                        <input
                            type="checkbox"
                            checked={todo.completed}
                            onChange={() => toggleTodo(todo.id)}
                        />
                        <span style={{ 
                            textDecoration: todo.completed ? 'line-through' : 'none' 
                        }}>
                            {todo.text}
                        </span>
                        <button onClick={() => removeTodo(todo.id)}>
                            Delete
                        </button>
                    </li>
                ))}
            </ul>
        </div>
    );
}
            

Functional Updates

When the new state depends on the previous state, use the functional form:


function Counter() {
    const [count, setCount] = useState(0);
    
    // ❌ Problematic with rapid clicks
    const incrementWrong = () => {
        setCount(count + 1);
        setCount(count + 1); // This won't work as expected
    };
    
    // ✅ Correct way using functional update
    const incrementRight = () => {
        setCount(prevCount => prevCount + 1);
        setCount(prevCount => prevCount + 1); // This works correctly
    };
    
    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={incrementRight}>Increment by 2</button>
        </div>
    );
}
            

Lazy Initial State

For expensive computations, you can pass a function to useState:


function ExpensiveComponent() {
    // ❌ This runs on every render
    const [data, setData] = useState(expensiveCalculation());
    
    // ✅ This runs only on initial render
    const [data, setData] = useState(() => expensiveCalculation());
    
    return <div>{/* component content */}</div>;
}

// Example with localStorage
function PersistentCounter() {
    const [count, setCount] = useState(() => {
        const saved = localStorage.getItem('count');
        return saved ? parseInt(saved) : 0;
    });
    
    useEffect(() => {
        localStorage.setItem('count', count);
    }, [count]);
    
    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={() => setCount(count + 1)}>Increment</button>
        </div>
    );
}
            

State Update Batching

React batches state updates for performance:


function BatchingExample() {
    const [count, setCount] = useState(0);
    const [flag, setFlag] = useState(false);
    
    function handleClick() {
        // React batches these updates
        setCount(c => c + 1); // Does not re-render yet
        setFlag(f => !f);     // Does not re-render yet
        // React will only re-render once at the end
    }
    
    console.log('Rendered with count:', count, 'and flag:', flag);
    
    return (
        <div>
            <button onClick={handleClick}>
                Update Both States
            </button>
            <p>Count: {count}, Flag: {flag.toString()}</p>
        </div>
    );
}
            

Common useState Patterns

1. Form Handling


function Form() {
    const [formData, setFormData] = useState({
        username: '',
        email: '',
        password: ''
    });
    
    const handleChange = (e) => {
        const { name, value } = e.target;
        setFormData(prevData => ({
            ...prevData,
            [name]: value
        }));
    };
    
    const handleSubmit = (e) => {
        e.preventDefault();
        console.log('Form submitted:', formData);
    };
    
    return (
        <form onSubmit={handleSubmit}>
            <input
                name="username"
                value={formData.username}
                onChange={handleChange}
                placeholder="Username"
            />
            <input
                name="email"
                type="email"
                value={formData.email}
                onChange={handleChange}
                placeholder="Email"
            />
            <input
                name="password"
                type="password"
                value={formData.password}
                onChange={handleChange}
                placeholder="Password"
            />
            <button type="submit">Submit</button>
        </form>
    );
}
            

2. Modal Management


function App() {
    const [isModalOpen, setIsModalOpen] = useState(false);
    
    return (
        <div>
            <button onClick={() => setIsModalOpen(true)}>
                Open Modal
            </button>
            
            {isModalOpen && (
                <Modal onClose={() => setIsModalOpen(false)}>
                    <h2>Modal Content</h2>
                    <p>This is a modal dialog</p>
                </Modal>
            )}
        </div>
    );
}
            

3. Loading States


function DataFetcher() {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState(null);
    
    const fetchData = async () => {
        setLoading(true);
        setError(null);
        
        try {
            const response = await fetch('/api/data');
            const json = await response.json();
            setData(json);
        } catch (err) {
            setError(err.message);
        } finally {
            setLoading(false);
        }
    };
    
    if (loading) return <div>Loading...</div>;
    if (error) return <div>Error: {error}</div>;
    if (!data) return <button onClick={fetchData}>Load Data</button>;
    
    return <div>{/* Display data */}</div>;
}
            

Rules of Hooks

graph TD A[Rules of Hooks] --> B[Only Call at Top Level] A --> C[Only Call from React Functions] B --> D[Not inside loops] B --> E[Not inside conditions] B --> F[Not inside nested functions] C --> G[React function components] C --> H[Custom Hooks] style A fill:#f96,stroke:#333,stroke-width:2px

// ❌ WRONG - Don't call Hooks inside conditions
function BadComponent({ shouldTrack }) {
    if (shouldTrack) {
        const [count, setCount] = useState(0); // Error!
    }
    // ...
}

// ✅ CORRECT - Call Hooks at the top level
function GoodComponent({ shouldTrack }) {
    const [count, setCount] = useState(0);
    
    if (shouldTrack) {
        // Use the state conditionally
    }
    // ...
}
            

Common Mistakes to Avoid

1. Directly Mutating State


// ❌ WRONG
function Wrong() {
    const [user, setUser] = useState({ name: 'John', age: 30 });
    
    const updateAge = () => {
        user.age = 31; // Direct mutation!
        setUser(user); // React won't detect the change
    };
}

// ✅ CORRECT
function Correct() {
    const [user, setUser] = useState({ name: 'John', age: 30 });
    
    const updateAge = () => {
        setUser(prevUser => ({
            ...prevUser,
            age: 31
        }));
    };
}
            

2. Stale Closure Issues


// ❌ Problem with setInterval
function Timer() {
    const [count, setCount] = useState(0);
    
    useEffect(() => {
        const interval = setInterval(() => {
            setCount(count + 1); // Always uses initial count value
        }, 1000);
        
        return () => clearInterval(interval);
    }, []); // Empty deps array
    
    return <div>Count: {count}</div>;
}

// ✅ Solution using functional update
function Timer() {
    const [count, setCount] = useState(0);
    
    useEffect(() => {
        const interval = setInterval(() => {
            setCount(prevCount => prevCount + 1); // Uses current value
        }, 1000);
        
        return () => clearInterval(interval);
    }, []);
    
    return <div>Count: {count}</div>;
}
            

Practice Exercises

Exercise 1: Color Picker

Create a color picker component that allows users to select RGB values:


function ColorPicker() {
    // Create state for red, green, and blue values (0-255)
    // Display the color preview
    // Show RGB values
    // Add sliders to adjust each color
}
            

Exercise 2: Shopping List

Build a shopping list with add, remove, and mark as purchased features:


function ShoppingList() {
    // Create state for items array
    // Add new items
    // Mark items as purchased
    // Remove items
    // Show total count and purchased count
}
            

Exercise 3: Password Strength Checker

Create a password input that shows strength indicator:


function PasswordInput() {
    // Track password value
    // Calculate strength based on:
    //   - Length
    //   - Contains numbers
    //   - Contains special characters
    //   - Contains uppercase and lowercase
    // Show strength indicator (weak/medium/strong)
}
            

Key Takeaways

What's Next?

In our next lesson, we'll explore the useEffect Hook, which allows you to perform side effects in your components. You'll learn how to fetch data, subscribe to events, and manage component lifecycle!

Homework