🎣 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:
- Only call Hooks at the top level - Don't call Hooks inside loops, conditions, or nested functions
- 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
🎯 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 callinginitialValue- The starting value of our statestateVariable- 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?
useState(0)creates a state variable with initial value 0- We get back two things: the current value (
count) and a function to update it (setCount) - We display the current count in the JSX
- When the button is clicked, we call
setCount(count + 1) - React re-renders the component with the new count value
- 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
nullorundefined - 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:
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:
💡 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!
📚 Additional Resources
🎉 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! 🚀