Skip to main content

🎣 useState Hook

Welcome to the world of React Hooks! Up until now, your components have been pretty static - displaying information but not really changing. But real applications need to remember things, respond to users, and update dynamically. That's where state comes in! The useState Hook is your gateway to making components truly interactive and alive. Think of it as giving your components a memory - they can remember values, change them over time, and trigger re-renders when things change. Let's unlock this superpower! 🚀

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Understand what React Hooks are and why they exist
  • Use the useState Hook to add state to functional components
  • Properly type state variables with TypeScript
  • Update state correctly and understand why it's important
  • Work with different state types: primitives, objects, and arrays
  • Use functional updates for state that depends on previous values
  • Build interactive components that respond to user actions
  • Debug common useState mistakes

Estimated Time: 70-85 minutes

Project: Build a counter, a toggle button, and a form with controlled inputs

📑 In This Lesson

🎣 Introduction to React Hooks

Before we dive into useState specifically, let's understand what Hooks are and why React introduced them.

📖 Definition

React Hooks: Functions that let you "hook into" React features from functional components. They let you use state and other React features without writing a class component.

Why Hooks?

React Hooks were introduced in React 16.8 (February 2019) to solve several problems:

Problem How Hooks Help
Class components were complex Functional components with Hooks are simpler
Difficult to reuse stateful logic Custom Hooks make logic reusable
Related code was scattered Hooks group related logic together
"this" keyword confusion No classes = no "this" to worry about

The Hook Rules

⚠️ Important: Rules of Hooks

Hooks have two critical rules that you must follow:

  1. Only call Hooks at the top level - Don't call Hooks inside loops, conditions, or nested functions
  2. Only call Hooks from React functions - Call them from functional components or custom Hooks

Why? React relies on the order Hooks are called to keep track of state. Changing the order breaks everything!

Common React Hooks

React provides several built-in Hooks. Here are the most important ones:

graph TD
    A[React Hooks] --> B[useState]
    A --> C[useEffect]
    A --> D[useContext]
    A --> E[useReducer]
    A --> F[useRef]
    A --> G[useMemo]
    A --> H[useCallback]
    
    B --> B1[Manage state]
    C --> C1[Side effects]
    D --> D1[Context values]
    E --> E1[Complex state logic]
    
    style B fill:#667eea,color:#fff
    style C fill:#764ba2,color:#fff
    style D fill:#f093fb,color:#fff
    style E fill:#2196F3,color:#fff

Today we're focusing on useState - the most fundamental and commonly used Hook!

💡 Good to Know

All React Hooks start with the word "use". This is a convention that helps you instantly recognize them. You can also create your own custom Hooks - and they should also start with "use"!

🧠 What is State?

Before we learn how to use useState, let's make sure we understand what "state" actually means in React.

📖 Definition

State: Data that changes over time in your component. It's information that the component needs to remember between renders. When state changes, the component re-renders to reflect the new state.

State vs Props vs Variables

It's crucial to understand how state differs from other types of data in React:

Type Purpose Changes? Triggers Re-render?
State Data owned by the component ✅ Yes, using setState ✅ Yes
Props Data passed from parent ❌ Read-only in component ✅ Yes (when parent changes them)
Regular Variables Temporary calculations ✅ Yes, but resets on re-render ❌ No

Analogy: State is Like Memory

🧠 Think of it this way:

Imagine you're taking notes during a lecture:

  • Props are like the lecture slides the professor shows you - you can read them but can't change them
  • State is like your notebook - you write things down, update them, and refer back to them later
  • Regular variables are like mental math you do in your head - they're gone once you move to the next problem

Your notebook (state) persists throughout the lecture, even as new slides (props) are shown!

When Do You Need State?

Use state when your component needs to remember information that:

  • Changes based on user interactions (clicks, input, etc.)
  • Changes over time (counters, timers)
  • Affects what the component displays
  • Needs to persist across re-renders

Examples of State in the Wild

Component State It Might Have
Counter Current count number
Form Input Current text value
Modal Dialog Whether it's open or closed
Todo List Array of todo items
Toggle Switch Whether it's on or off
Shopping Cart Items in cart, total price

🎯 Visual Comparison

State vs Props vs Variables 🎣 STATE useState() Memory ✓ Component owns it ✓ Can be changed ✓ Triggers re-render ✓ Persists between renders const [count, setCount] = useState(0); // Changes? Re-render! 📦 PROPS From Parent Read-only ✓ Passed from parent ✗ Cannot modify ✓ Changes cause re-render ✓ Flows down the tree function Card({ title }) // title is read-only // Can't: title = "new" 📝 VARIABLES const/let Temporary ✓ Local to render ✓ Can be changed ✗ No re-render ✗ Lost on re-render const doubled = count * 2; // Recalculated each // render from scratch

🎯 useState Basics

Let's start using useState! We'll begin with the simplest possible example and build from there.

Basic Syntax

The useState Pattern

import { useState } from 'react';

const [stateVariable, setStateVariable] = useState(initialValue);

Let's break this down:

  • useState - The Hook function we're calling
  • initialValue - The starting value of our state
  • stateVariable - The current state value (read-only)
  • setStateVariable - Function to update the state
  • [...] - Array destructuring to get both values

Your First useState Example: A Counter

Simple Counter Component

import React, { useState } from 'react';

const Counter: React.FC = () => {
    // Declare a state variable called 'count' with initial value 0
    const [count, setCount] = useState(0);

    return (
        <div>
            <h2>Counter: {count}</h2>
            <button onClick={() => setCount(count + 1)}>
                Increment
            </button>
        </div>
    );
};

export default Counter;

💡 What's Happening Here?

  1. useState(0) creates a state variable with initial value 0
  2. We get back two things: the current value (count) and a function to update it (setCount)
  3. We display the current count in the JSX
  4. When the button is clicked, we call setCount(count + 1)
  5. React re-renders the component with the new count value
  6. The display updates automatically!

How Re-rendering Works

sequenceDiagram
    participant User
    participant React
    participant Component
    
    User->>Component: Click button
    Component->>React: Call setCount(count + 1)
    React->>React: Update state
    React->>Component: Re-render with new state
    Component->>User: Display new count
    
    Note over React,Component: Component function runs again
with new state value

✅ Key Insight

State updates trigger re-renders. When you call the setter function (like setCount), React schedules a re-render of your component. On the next render, useState returns the updated value.

Naming Conventions

By convention, we name the state variable and its setter following this pattern:

const [thing, setThing] = useState(initialValue);

// Examples:
const [count, setCount] = useState(0);
const [name, setName] = useState('');
const [isOpen, setIsOpen] = useState(false);
const [items, setItems] = useState([]);
const [user, setUser] = useState(null);

The setter always starts with "set" followed by the capitalized state variable name. This makes it immediately clear what it does!

🎮 Interactive Demo: Watch the Re-render Cycle

Click the buttons and watch how React updates the component:

Watch the state update cycle in action!

📘 Typing State with TypeScript

One of the great things about using TypeScript with React is that useState is fully type-safe. Let's learn how to properly type your state!

Type Inference

TypeScript Infers Simple Types

For primitive values, TypeScript can usually infer the type automatically:

// TypeScript infers these types automatically
const [count, setCount] = useState(0);           // number
const [name, setName] = useState('');            // string
const [isOpen, setIsOpen] = useState(false);     // boolean
const [items, setItems] = useState([]);          // never[] (not useful!)
const [user, setUser] = useState(null);          // null (not useful!)

Notice the last two - arrays and null don't infer useful types. We need to be explicit!

Explicit Type Annotations

Using Generics with useState

For complex types, use TypeScript generics to specify the type:

import { useState } from 'react';

interface User {
    id: number;
    name: string;
    email: string;
}

const UserProfile: React.FC = () => {
    // Explicitly type the state
    const [user, setUser] = useState<User | null>(null);
    const [items, setItems] = useState<string[]>([]);
    const [count, setCount] = useState<number>(0);
    
    return (
        <div>
            {user ? (
                <p>Welcome, {user.name}!</p>
            ) : (
                <p>Please log in</p>
            )}
        </div>
    );
};

Common Typing Patterns

State Type TypeScript Syntax Example
String useState<string>('') const [name, setName] = useState<string>('')
Number useState<number>(0) const [age, setAge] = useState<number>(0)
Boolean useState<boolean>(false) const [isOpen, setIsOpen] = useState<boolean>(false)
Array useState<Type[]>([]) const [items, setItems] = useState<string[]>([])
Object useState<Interface>(obj) const [user, setUser] = useState<User>(initialUser)
Nullable useState<Type | null>(null) const [user, setUser] = useState<User | null>(null)
Union Types useState<'A' | 'B'>('A') const [status, setStatus] = useState<'idle' | 'loading'>('idle')

Type-Safe State Updates

TypeScript Checks Your Updates

interface Todo {
    id: number;
    text: string;
    completed: boolean;
}

const TodoList: React.FC = () => {
    const [todos, setTodos] = useState<Todo[]>([]);
    
    const addTodo = (text: string) => {
        const newTodo: Todo = {
            id: Date.now(),
            text: text,
            completed: false
        };
        
        // ✅ TypeScript knows this is correct
        setTodos([...todos, newTodo]);
        
        // ❌ TypeScript error - wrong type!
        // setTodos([...todos, { text: 'incomplete' }]);
        
        // ❌ TypeScript error - can't assign string to Todo[]
        // setTodos('not an array');
    };
    
    return <div>{/* JSX */}</div>;
};

✅ Pro Tip: When to Be Explicit

Use explicit type annotations when:

  • Working with arrays (otherwise you get never[])
  • Initial value is null or undefined
  • Using complex objects or interfaces
  • You want to be extra clear about the type

For simple primitives with non-null initial values, type inference works great!

✏️ Updating State Correctly

Understanding how to update state correctly is crucial. There are some important rules and patterns you need to know!

State Updates are Asynchronous

⚠️ Important: State Updates Don't Happen Immediately

When you call setState, React doesn't update the state right away. It schedules an update for the next render.

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

const handleClick = () => {
    setCount(count + 1);
    console.log(count);  // ⚠️ Still 0! Not updated yet!
    
    // This won't work as expected
    setCount(count + 1);  // Still using old count (0)
    setCount(count + 1);  // Still using old count (0)
    // Result: count becomes 1, not 3!
};

State is Immutable

Never Mutate State Directly

State should be treated as read-only. Always create new values instead of modifying existing ones:

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

// ❌ WRONG - Don't do this!
count = count + 1;  // Error: can't reassign const
count++;            // Error: can't reassign const

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


const [user, setUser] = useState({ name: 'Alice', age: 25 });

// ❌ WRONG - Don't mutate the object directly
user.age = 26;              // Mutation! Won't trigger re-render
setUser(user);              // Still wrong - same object reference

// ✅ CORRECT - Create a new object
setUser({ ...user, age: 26 });


const [items, setItems] = useState(['a', 'b', 'c']);

// ❌ WRONG - Don't mutate the array directly
items.push('d');            // Mutation! Won't trigger re-render
setItems(items);            // Still wrong - same array reference

// ✅ CORRECT - Create a new array
setItems([...items, 'd']);

Why Immutability Matters

graph TD
    A[setState called] --> B{Did reference change?}
    B -->|Yes - New object/array| C[React re-renders]
    B -->|No - Same reference| D[React skips re-render]
    
    C --> E[UI updates ✅]
    D --> F[UI doesn't update ❌]
    
    style C fill:#4CAF50,color:#fff
    style D fill:#f44336,color:#fff
    style E fill:#4CAF50,color:#fff
    style F fill:#f44336,color:#fff

React uses reference equality (===) to check if state changed. If you mutate the original object/array, it's still the same reference, so React won't detect the change!

🔍 Reference Equality Explained

React uses reference equality (===) to detect changes. Here's why mutation doesn't work:

Why Mutation Doesn't Trigger Re-renders ❌ Mutation (WRONG) Memory Address: 0x1234 user object name: "Alice" age: 25 → 26 (mutated in place) same ref user.age = 26; setUser(user); React checks: oldRef === newRef? 0x1234 === 0x1234 → TRUE "No change detected!" → Skip re-render ✅ New Object (CORRECT) Memory Addresses: old: 0x1234 name: "Alice" age: 25 new: 0x5678 name: "Alice" age: 26 setUser({ ...user, age: 26 }); React checks: oldRef === newRef? 0x1234 === 0x5678 → FALSE "Change detected!" → Re-render! ✅

Multiple Updates in One Event

React Batches Updates

React batches multiple state updates in the same event handler for performance:

const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);

const handleClick = () => {
    setCount(count + 1);    // Doesn't re-render yet
    setFlag(!flag);          // Doesn't re-render yet
    // React batches these and re-renders once after the function completes
};

// Result: One re-render with both updates applied ✅

💡 Mental Model

Think of state updates like ordering at a restaurant:

  • You tell the waiter what you want (call setState)
  • The waiter writes it down (React queues the update)
  • After everyone at the table orders (event handler completes)
  • The waiter brings everything at once (React re-renders with all updates)

This is more efficient than bringing one item at a time!

🎁 State with Objects

Working with objects in state requires special care. You need to create new objects instead of modifying existing ones.

Basic Object State

Updating Object Properties

interface User {
    name: string;
    email: string;
    age: number;
}

const UserForm: React.FC = () => {
    const [user, setUser] = useState<User>({
        name: '',
        email: '',
        age: 0
    });

    // Update a single property
    const updateName = (newName: string) => {
        setUser({
            ...user,        // Copy all existing properties
            name: newName   // Override the name property
        });
    };

    // Update multiple properties
    const updateUser = (name: string, email: string) => {
        setUser({
            ...user,
            name: name,
            email: email
        });
    };

    return (
        <div>
            <input 
                value={user.name}
                onChange={(e) => setUser({ ...user, name: e.target.value })}
            />
            <input 
                value={user.email}
                onChange={(e) => setUser({ ...user, email: e.target.value })}
            />
        </div>
    );
};

Nested Objects

Updating Nested Properties

For nested objects, you need to copy at each level:

interface Address {
    street: string;
    city: string;
    zipCode: string;
}

interface User {
    name: string;
    address: Address;
}

const UserProfile: React.FC = () => {
    const [user, setUser] = useState<User>({
        name: 'Alice',
        address: {
            street: '123 Main St',
            city: 'Boston',
            zipCode: '02101'
        }
    });

    // ❌ WRONG - This mutates the nested object
    const updateCityWrong = (newCity: string) => {
        user.address.city = newCity;
        setUser(user);
    };

    // ✅ CORRECT - Copy both levels
    const updateCity = (newCity: string) => {
        setUser({
            ...user,                    // Copy user
            address: {
                ...user.address,        // Copy address
                city: newCity           // Update city
            }
        });
    };

    return (
        <div>
            <input 
                value={user.address.city}
                onChange={(e) => updateCity(e.target.value)}
            />
        </div>
    );
};

Patterns for Object Updates

Operation Pattern
Update one property setObj({ ...obj, key: newValue })
Update multiple properties setObj({ ...obj, key1: val1, key2: val2 })
Update nested property setObj({ ...obj, nested: { ...obj.nested, key: val } })
Replace entire object setObj(newObject)
Reset to initial setObj(initialState)

✅ Pro Tip: Helper Function Pattern

For complex updates, create helper functions:

const updateUserField = (field: keyof User, value: any) => {
    setUser({ ...user, [field]: value });
};

// Usage:
updateUserField('name', 'Bob');
updateUserField('email', '[email protected]');

📚 State with Arrays

Arrays in state also require immutable updates. Let's learn all the common array operations!

Adding Items

Adding to Arrays

const [items, setItems] = useState<string[]>([]);

// Add to end
setItems([...items, 'new item']);

// Add to beginning
setItems(['new item', ...items]);

// Add at specific index
const index = 2;
setItems([
    ...items.slice(0, index),
    'new item',
    ...items.slice(index)
]);

Removing Items

Removing from Arrays

const [items, setItems] = useState<string[]>(['a', 'b', 'c']);

// Remove by index
const removeAtIndex = (indexToRemove: number) => {
    setItems(items.filter((_, index) => index !== indexToRemove));
};

// Remove by value
const removeByValue = (valueToRemove: string) => {
    setItems(items.filter(item => item !== valueToRemove));
};

// Remove first item
setItems(items.slice(1));

// Remove last item
setItems(items.slice(0, -1));

Updating Items

Modifying Array Items

interface Todo {
    id: number;
    text: string;
    completed: boolean;
}

const [todos, setTodos] = useState<Todo[]>([]);

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

// Update item at index
const updateAtIndex = (index: number, newText: string) => {
    setTodos(todos.map((todo, i) =>
        i === index
            ? { ...todo, text: newText }
            : todo
    ));
};

Common Array Operations

Operation Pattern Notes
Add to end [...arr, newItem] Like push()
Add to start [newItem, ...arr] Like unshift()
Remove arr.filter(item => condition) Returns new array
Update arr.map(item => condition ? newItem : item) Returns new array
Replace all setArr(newArray) Complete replacement
Clear all setArr([]) Empty array
Sort [...arr].sort() Copy first! sort() mutates
Reverse [...arr].reverse() Copy first! reverse() mutates

🎮 Interactive Demo: Array State Operations

See how immutable array operations work in real-time:

⚠️ Watch Out: Mutating Methods

These array methods mutate the original array - don't use them directly on state:

  • push(), pop(), shift(), unshift()
  • splice()
  • sort(), reverse()

Always create a copy first if you need to use these!

Complete Todo List Example

Putting It All Together

import React, { useState } from 'react';

interface Todo {
    id: number;
    text: string;
    completed: boolean;
}

const TodoList: React.FC = () => {
    const [todos, setTodos] = useState<Todo[]>([]);
    const [inputText, setInputText] = useState('');

    // Add new todo
    const addTodo = () => {
        if (inputText.trim()) {
            setTodos([...todos, {
                id: Date.now(),
                text: inputText,
                completed: false
            }]);
            setInputText('');
        }
    };

    // Toggle completion
    const toggleTodo = (id: number) => {
        setTodos(todos.map(todo =>
            todo.id === id
                ? { ...todo, completed: !todo.completed }
                : todo
        ));
    };

    // Delete todo
    const deleteTodo = (id: number) => {
        setTodos(todos.filter(todo => todo.id !== id));
    };

    return (
        <div>
            <input 
                value={inputText}
                onChange={(e) => setInputText(e.target.value)}
                onKeyPress={(e) => e.key === 'Enter' && addTodo()}
            />
            <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={() => deleteTodo(todo.id)}>
                            Delete
                        </button>
                    </li>
                ))}
            </ul>
        </div>
    );
};

export default TodoList;

⚙️ Functional Updates

Sometimes you need to update state based on the previous state value. There's a special pattern for this!

The Problem with Direct Updates

⚠️ This Can Go Wrong

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

const increment = () => {
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
    // You might expect count to be 3, but it will be 1!
    // All three updates use the same old value of count
};

The Solution: Functional Updates

Pass a Function to setState

Instead of passing the new value, pass a function that receives the previous value:

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

// ✅ CORRECT - Use functional update
const increment = () => {
    setCount(prevCount => prevCount + 1);
    setCount(prevCount => prevCount + 1);
    setCount(prevCount => prevCount + 1);
    // Now count will be 3! ✅
};

// The pattern:
setCount(previousValue => newValue);

When to Use Functional Updates

Situation Use Functional Update?
New state depends on old state ✅ Yes
Multiple updates in same function ✅ Yes
Update in useEffect or callback ✅ Yes (safer)
Setting to a specific value ❌ No, direct is fine

🔄 The Stale Closure Problem Visualized

This diagram shows why functional updates are essential when making multiple updates:

Direct Updates vs Functional Updates ❌ Direct Updates (Stale Closure) count = 0 ← captured at render setCount(count + 1) // count is 0 → sets to 1 setCount(count + 1) // count is 0 → sets to 1 setCount(count + 1) // count is 0 → sets to 1 React batches: only last value (1) is used Result: count = 1 Expected 3, got 1! ❌ ✅ Functional Updates count = 0 ← gets fresh each time setCount(prev => prev + 1) // 0 → 1 setCount(prev => prev + 1) // 1 → 2 setCount(prev => prev + 1) // 2 → 3 React chains: each update uses previous result Result: count = 3 All updates applied correctly! ✅

💡 When to Use Each Pattern

setCount(count + 1) Fine for single updates based on events
setCount(prev => prev + 1) Required for multiple updates, or in callbacks/effects

Examples with Different Types

Functional Updates for Various State Types

// Numbers
const [count, setCount] = useState(0);
setCount(prev => prev + 1);
setCount(prev => prev * 2);

// Booleans
const [isOpen, setIsOpen] = useState(false);
setIsOpen(prev => !prev);

// Strings
const [text, setText] = useState('');
setText(prev => prev + ' more text');

// Objects
const [user, setUser] = useState({ name: '', age: 0 });
setUser(prev => ({ ...prev, age: prev.age + 1 }));

// Arrays
const [items, setItems] = useState<number[]>([]);
setItems(prev => [...prev, prev.length + 1]);
setItems(prev => prev.filter(item => item > 5));
setItems(prev => prev.map(item => item * 2));

✅ Best Practice

When in doubt, use functional updates! They're always safe and often more correct than direct updates. The convention is to name the parameter prev or previous followed by the state name:

setCount(prevCount => prevCount + 1);
setUser(prevUser => ({ ...prevUser, name: 'New' }));
setItems(prevItems => [...prevItems, newItem]);

🏋️ Hands-on Practice

Time to apply what you've learned! Let's build some practical components using useState.

🏋️ Exercise 1: Enhanced Counter

Goal: Build a counter with increment, decrement, and reset buttons.

Requirements:

  • Display current count
  • Increment button (+1)
  • Decrement button (-1)
  • Reset button (back to 0)
  • Prevent count from going below 0
💡 Hint

Use a single state variable and conditional logic:

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

const decrement = () => {
    if (count > 0) {
        setCount(count - 1);
    }
    // Or: setCount(prev => Math.max(0, prev - 1));
};
✅ Solution
import React, { useState } from 'react';

const EnhancedCounter: React.FC = () => {
    const [count, setCount] = useState(0);

    const increment = () => {
        setCount(prevCount => prevCount + 1);
    };

    const decrement = () => {
        setCount(prevCount => Math.max(0, prevCount - 1));
    };

    const reset = () => {
        setCount(0);
    };

    return (
        <div style={{ textAlign: 'center', padding: '2rem' }}>
            <h2>Count: {count}</h2>
            <div style={{ display: 'flex', gap: '1rem', justifyContent: 'center' }}>
                <button 
                    onClick={decrement}
                    disabled={count === 0}
                    style={{ padding: '0.5rem 1rem' }}
                >
                    -1
                </button>
                <button 
                    onClick={reset}
                    style={{ padding: '0.5rem 1rem' }}
                >
                    Reset
                </button>
                <button 
                    onClick={increment}
                    style={{ padding: '0.5rem 1rem' }}
                >
                    +1
                </button>
            </div>
        </div>
    );
};

export default EnhancedCounter;

🏋️ Exercise 2: Text Input with Character Count

Goal: Create a text area that displays remaining characters.

Requirements:

  • Text area for input
  • Maximum 280 characters
  • Display remaining characters
  • Change color when close to limit (under 20)
  • Prevent input when at limit
💡 Hint

Use state for the text and calculate remaining:

const [text, setText] = useState('');
const maxLength = 280;
const remaining = maxLength - text.length;
const isNearLimit = remaining < 20;
✅ Solution
import React, { useState } from 'react';

const CharacterCounter: React.FC = () => {
    const [text, setText] = useState('');
    const maxLength = 280;
    const remaining = maxLength - text.length;
    const isNearLimit = remaining < 20;
    const isAtLimit = remaining === 0;

    const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
        const newText = e.target.value;
        if (newText.length <= maxLength) {
            setText(newText);
        }
    };

    return (
        <div style={{ maxWidth: '600px', margin: '0 auto', padding: '2rem' }}>
            <textarea
                value={text}
                onChange={handleChange}
                placeholder="What's on your mind?"
                style={{
                    width: '100%',
                    minHeight: '120px',
                    padding: '1rem',
                    fontSize: '1rem',
                    border: `2px solid ${isAtLimit ? '#f44336' : '#ddd'}`,
                    borderRadius: '8px',
                    resize: 'vertical'
                }}
            />
            
            <div style={{
                marginTop: '0.5rem',
                textAlign: 'right',
                fontSize: '1rem',
                fontWeight: 'bold',
                color: isAtLimit ? '#f44336' : isNearLimit ? '#ff9800' : '#4CAF50'
            }}>
                {remaining} characters remaining
            </div>
            
            {isAtLimit && (
                <p style={{ color: '#f44336', marginTop: '0.5rem' }}>
                    Character limit reached!
                </p>
            )}
        </div>
    );
};

export default CharacterCounter;

🏋️ Exercise 3: User Registration Form

Goal: Build a form that manages multiple related state values.

Requirements:

  • Fields: username, email, password, confirm password
  • Display all values as user types
  • Show validation messages
  • Check if passwords match
  • Show success message on "submit"
💡 Hint

Use object state for form data:

interface FormData {
    username: string;
    email: string;
    password: string;
    confirmPassword: string;
}

const [formData, setFormData] = useState<FormData>({
    username: '',
    email: '',
    password: '',
    confirmPassword: ''
});
✅ Solution
import React, { useState } from 'react';

interface FormData {
    username: string;
    email: string;
    password: string;
    confirmPassword: string;
}

const RegistrationForm: React.FC = () => {
    const [formData, setFormData] = useState<FormData>({
        username: '',
        email: '',
        password: '',
        confirmPassword: ''
    });
    
    const [isSubmitted, setIsSubmitted] = useState(false);

    const handleChange = (field: keyof FormData, value: string) => {
        setFormData(prev => ({
            ...prev,
            [field]: value
        }));
    };

    const passwordsMatch = formData.password === formData.confirmPassword;
    const isFormValid = 
        formData.username.length >= 3 &&
        formData.email.includes('@') &&
        formData.password.length >= 6 &&
        passwordsMatch;

    const handleSubmit = (e: React.FormEvent) => {
        e.preventDefault();
        if (isFormValid) {
            setIsSubmitted(true);
            console.log('Form submitted:', formData);
        }
    };

    if (isSubmitted) {
        return (
            <div style={{ maxWidth: '400px', margin: '0 auto', padding: '2rem', textAlign: 'center' }}>
                <h2 style={{ color: '#4CAF50' }}>✅ Registration Successful!</h2>
                <p>Welcome, {formData.username}!</p>
                <button onClick={() => setIsSubmitted(false)}>
                    Register Another User
                </button>
            </div>
        );
    }

    return (
        <form onSubmit={handleSubmit} style={{ maxWidth: '400px', margin: '0 auto', padding: '2rem' }}>
            <h2>User Registration</h2>

            <div style={{ marginBottom: '1rem' }}>
                <label style={{ display: 'block', marginBottom: '0.5rem' }}>
                    Username:
                </label>
                <input
                    type="text"
                    value={formData.username}
                    onChange={(e) => handleChange('username', e.target.value)}
                    style={{ width: '100%', padding: '0.5rem' }}
                />
                {formData.username && formData.username.length < 3 && (
                    <small style={{ color: '#f44336' }}>
                        Username must be at least 3 characters
                    </small>
                )}
            </div>

            <div style={{ marginBottom: '1rem' }}>
                <label style={{ display: 'block', marginBottom: '0.5rem' }}>
                    Email:
                </label>
                <input
                    type="email"
                    value={formData.email}
                    onChange={(e) => handleChange('email', e.target.value)}
                    style={{ width: '100%', padding: '0.5rem' }}
                />
            </div>

            <div style={{ marginBottom: '1rem' }}>
                <label style={{ display: 'block', marginBottom: '0.5rem' }}>
                    Password:
                </label>
                <input
                    type="password"
                    value={formData.password}
                    onChange={(e) => handleChange('password', e.target.value)}
                    style={{ width: '100%', padding: '0.5rem' }}
                />
                {formData.password && formData.password.length < 6 && (
                    <small style={{ color: '#f44336' }}>
                        Password must be at least 6 characters
                    </small>
                )}
            </div>

            <div style={{ marginBottom: '1rem' }}>
                <label style={{ display: 'block', marginBottom: '0.5rem' }}>
                    Confirm Password:
                </label>
                <input
                    type="password"
                    value={formData.confirmPassword}
                    onChange={(e) => handleChange('confirmPassword', e.target.value)}
                    style={{ width: '100%', padding: '0.5rem' }}
                />
                {formData.confirmPassword && !passwordsMatch && (
                    <small style={{ color: '#f44336' }}>
                        Passwords don't match
                    </small>
                )}
            </div>

            <button 
                type="submit"
                disabled={!isFormValid}
                style={{
                    width: '100%',
                    padding: '0.75rem',
                    backgroundColor: isFormValid ? '#667eea' : '#ccc',
                    color: 'white',
                    border: 'none',
                    borderRadius: '4px',
                    cursor: isFormValid ? 'pointer' : 'not-allowed',
                    fontSize: '1rem',
                    fontWeight: 'bold'
                }}
            >
                Register
            </button>
        </form>
    );
};

export default RegistrationForm;

🏋️ Challenge Exercise: Shopping Cart

Goal: Build a mini shopping cart with add, remove, and quantity controls.

Requirements:

  • List of products with "Add to Cart" buttons
  • Cart that shows added items
  • Quantity controls (+/-) for each cart item
  • Remove item button
  • Display total price
  • Use array state for cart items
💡 Hint
interface Product {
    id: number;
    name: string;
    price: number;
}

interface CartItem extends Product {
    quantity: number;
}

const [cart, setCart] = useState<CartItem[]>([]);
✅ Solution

Try this one on your own first! It combines everything you've learned about arrays, objects, and functional updates.

🚫 Common Mistakes

Let's look at the most common useState mistakes and how to avoid them.

❌ Mistake 1: Mutating State Directly

// ❌ WRONG
const [user, setUser] = useState({ name: 'Alice', age: 25 });
user.age = 26;  // Mutation!
setUser(user);  // Won't trigger re-render

// ✅ CORRECT
setUser({ ...user, age: 26 });

❌ Mistake 2: Using State Value Immediately After Setting

// ❌ WRONG
const [count, setCount] = useState(0);
setCount(5);
console.log(count);  // Still 0! Update is async

// ✅ CORRECT - Use the value you're setting
setCount(5);
console.log(5);  // Or just trust it will be 5 on next render

❌ Mistake 3: Creating State Inside Conditionals

// ❌ WRONG - Breaks Hook rules
if (someCondition) {
    const [count, setCount] = useState(0);  // Error!
}

// ✅ CORRECT - Always at top level
const [count, setCount] = useState(0);
if (someCondition) {
    // Use count here
}

❌ Mistake 4: Not Using Functional Updates

// ❌ WRONG - Multiple updates use stale value
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);  // count only increases by 1!

// ✅ CORRECT - Each update uses fresh value
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);  // count increases by 3 ✅

❌ Mistake 5: Forgetting to Copy Nested Structures

// ❌ WRONG - Shallow copy doesn't copy nested objects
const [user, setUser] = useState({
    name: 'Alice',
    address: { city: 'Boston' }
});
setUser({ ...user, address: { city: 'NYC' } });  // Loses other address fields!

// ✅ CORRECT - Copy all levels
setUser({
    ...user,
    address: { ...user.address, city: 'NYC' }
});

❌ Mistake 6: Overusing State

// ❌ WRONG - fullName doesn't need to be state
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');  // Redundant!

// ✅ CORRECT - Derive it instead
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = `${firstName} ${lastName}`;  // Just calculate it!

✨ Best Practices

Follow these guidelines to write clean, efficient, and maintainable useState code.

✅ 1. Keep State Minimal

Only store what you can't calculate from existing data:

// Good - Only store what's needed
const [todos, setTodos] = useState<Todo[]>([]);
const completedCount = todos.filter(t => t.completed).length;
const remainingCount = todos.length - completedCount;

// Bad - Storing derived values
const [todos, setTodos] = useState<Todo[]>([]);
const [completedCount, setCompletedCount] = useState(0);  // Can be calculated!
const [remainingCount, setRemainingCount] = useState(0);  // Can be calculated!

✅ 2. Group Related State

If values always change together, keep them in one object:

// Good - Related fields in one object
const [formData, setFormData] = useState({
    firstName: '',
    lastName: '',
    email: ''
});

// Less good - Separate states that change together
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');

✅ 3. Use Descriptive Names

// Good - Clear what it represents
const [isModalOpen, setIsModalOpen] = useState(false);
const [userProfile, setUserProfile] = useState(null);
const [searchQuery, setSearchQuery] = useState('');

// Bad - Vague names
const [flag, setFlag] = useState(false);
const [data, setData] = useState(null);
const [text, setText] = useState('');

✅ 4. Initialize with the Right Type

// Good - Clear initial values
const [count, setCount] = useState(0);
const [user, setUser] = useState<User | null>(null);
const [items, setItems] = useState<string[]>([]);

// Bad - Unclear or wrong initial values
const [count, setCount] = useState();  // undefined
const [user, setUser] = useState({});  // Empty object (not typed!)
const [items, setItems] = useState();  // undefined

✅ 5. Use Functional Updates When Appropriate

// Good - Safe for all situations
const increment = () => {
    setCount(prevCount => prevCount + 1);
};

// Also good - When you're sure it's safe
const setToSpecificValue = () => {
    setCount(42);  // Not based on previous value
};

✅ 6. Extract Complex Update Logic

// Good - Readable and reusable
const addTodo = (text: string) => {
    const newTodo = {
        id: Date.now(),
        text,
        completed: false
    };
    setTodos(prev => [...prev, newTodo]);
};

const toggleTodo = (id: number) => {
    setTodos(prev => prev.map(todo =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
};

useState Checklist

Question Check
Can this value be calculated from existing state/props? Don't store it
Do these values always change together? Group them
Am I mutating state directly? Create new objects/arrays
Does update depend on previous value? Use functional update
Is my TypeScript type explicit enough? Add generic if needed
Are Hooks at the top level? Never in conditionals/loops

📚 Summary

Congratulations! You've learned everything you need to know about the useState Hook. Let's recap the key concepts.

What You Learned

🎣 React Hooks Basics

  • Hooks let you use React features in functional components
  • Must be called at the top level (not in loops or conditionals)
  • Must be called from React functions

🧠 State Fundamentals

  • State is data that changes over time
  • State persists between renders
  • Updating state triggers re-renders
  • State is different from props and regular variables

🎯 useState Hook

  • Syntax: const [value, setValue] = useState(initial)
  • Returns current value and setter function
  • Use TypeScript generics for complex types
  • Updates are asynchronous (not immediate)

✏️ Updating State

  • Never mutate state directly
  • Always create new objects/arrays
  • Use spread operator for copying
  • Use functional updates when depending on previous value

📦 State with Objects and Arrays

  • Objects: setState({ ...obj, key: value })
  • Arrays: Use map(), filter(), spread operator
  • Avoid mutating methods like push(), sort()
  • Copy nested structures at every level

🎯 Key Takeaways

  • useState is your state management tool - It gives components memory
  • Immutability is crucial - Never mutate, always create new values
  • TypeScript makes it safer - Type your state for better DX
  • Functional updates are safer - Use them when in doubt
  • Keep state minimal - Don't store what you can calculate

🚀 Next Steps

Now that you've mastered useState, you're ready to learn more advanced state concepts:

📖 Coming Up Next

Lesson 3.2: State Management Patterns

  • Lifting state up between components
  • Prop drilling and how to avoid it
  • State colocation strategies
  • Derived state patterns

These patterns will help you organize state in larger applications!

💪 Practice Suggestions

To master useState, try building:

  • Temperature Converter - Celsius to Fahrenheit with live updates
  • Quiz App - Track current question, score, and user answers
  • Expense Tracker - Add expenses, categorize them, show totals
  • Color Picker - RGB sliders that update a preview box
  • Multi-step Form - Navigate between steps while preserving data

✨ Remember

useState is the foundation of React interactivity. The patterns you've learned here will be used in every React application you build. Take time to practice and experiment - the more you use it, the more natural it will become!

🎉 Congratulations!

You've completed Lesson 3.1 and learned how to make your React components interactive and stateful! You now understand one of the most important concepts in React. Well done! 🚀

← Previous Module 2 Project Home Next → Lesson 3.2: State Management Patterns