What is State in React?
State represents the data that can change over the lifetime of a component. It's essentially a component's memory, allowing it to track, respond to, and reflect changes in data as users interact with the application.
State is critical for creating dynamic, interactive applications because:
- It allows components to respond to user input and other events
- It enables dynamic rendering of content based on changing data
- It serves as the "source of truth" for the interactive parts of your interface
- It tracks values that should cause a component to re-render when changed
Analogy: State as a Component's Memory
Think of a component as a person and state as that person's memory:
- Just like humans remember things (like names, faces, events), components need to remember data (like form inputs, toggle states, selected options)
- When a person's memory changes (learns something new), their behavior might change. Similarly, when a component's state changes, it re-renders
- People have private memories that others can't directly access. Similarly, a component's state is private to that component (unless explicitly shared)
- When we forget something, we need someone to remind us. Similarly, when a component unmounts, its state is lost unless saved elsewhere
State vs. Props
Before diving deeper into state, it's important to understand the difference between state and props:
| State | Props |
|---|---|
| Internal to a component | Passed from parent to child |
| Can be modified by the component | Read-only (immutable) |
| Changes trigger re-renders | New props trigger re-renders |
| Persists between re-renders | Updated on each re-render of parent |
| Should contain minimal required data | Can contain any type of data |
Props vs. State Example
// Parent component passing props
function Parent() {
// This is state in the Parent
const [count, setCount] = useState(0);
// Handler to update state
const incrementCount = () => {
setCount(count + 1);
};
return (
<div>
<h2>Parent Count: {count}</h2>
<button onClick={incrementCount}>Increment</button>
{/* Passing state as props to child */}
<Child count={count} onIncrement={incrementCount} />
</div>
);
}
// Child receives props, but also has its own state
function Child({ count, onIncrement }) {
// This is state internal to the Child
const [childCount, setChildCount] = useState(0);
const incrementChildCount = () => {
setChildCount(childCount + 1);
};
return (
<div className="child">
<h3>Received from Parent: {count}</h3>
<button onClick={onIncrement}>Increment Parent Count</button>
<h3>Child's Own Count: {childCount}</h3>
<button onClick={incrementChildCount}>Increment Child Count</button>
</div>
);
}
When to Use State vs. Props
- Use State when:
- Data changes over time within a component
- User interactions should affect the displayed content
- You need to maintain component-specific data
- Use Props when:
- Passing data down from parent to child
- Configuring a component from outside
- Sharing data between components
Types of State
State can be categorized in different ways based on its purpose and scope:
Local vs. Global State
- Local State: Confined to a single component (e.g., form inputs, toggle status)
- Global State: Shared across multiple components (e.g., user authentication, theme settings)
UI State vs. Server State
- UI State: Controls how the interface behaves (e.g., modal open/closed, active tab)
- Server State: Data fetched from an external API (e.g., user profile, product list)
Common Types of State by Function
Examples of Different State Types
// Local UI State
function Accordion() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<h3 onClick={() => setIsOpen(!isOpen)}>
Click to {isOpen ? 'collapse' : 'expand'}
</h3>
{isOpen && <p>This content is dynamically shown/hidden.</p>}
</div>
);
}
// Form State
function LoginForm() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
// More form logic...
}
// Application State (often managed with Context or Redux)
function ThemeProvider() {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
// Provide theme context to the application...
}
// Server Cache State (often managed with React Query or SWR)
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUser(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]);
// Render based on loading, error, and user state...
}
State in Class Components
Before hooks were introduced in React 16.8, state was primarily managed in class components using this.state and this.setState().
Basic State in Class Components
import React, { Component } from 'react';
class Counter extends Component {
constructor(props) {
super(props);
// Initialize state in the constructor
this.state = {
count: 0
};
// Bind event handlers
this.increment = this.increment.bind(this);
this.decrement = this.decrement.bind(this);
}
increment() {
// Update state with setState
this.setState({ count: this.state.count + 1 });
}
decrement() {
this.setState({ count: this.state.count - 1 });
}
render() {
return (
<div>
<h2>Count: {this.state.count}</h2>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
</div>
);
}
}
Important Characteristics of Class Component State
- Initialization: State is initialized in the constructor by assigning an object to
this.state - Accessing: State is accessed via
this.stateproperties - Updating: State is updated only via the
setState()method, never by direct assignment - Asynchronous Updates: State updates may be asynchronous for performance reasons
- Partial Updates:
setState()merges the provided object with the current state
Functional Updates with setState
When new state depends on previous state, a functional update form should be used:
// Incorrect: May lead to stale state issues
increment() {
this.setState({ count: this.state.count + 1 });
}
// Correct: Using functional update
increment() {
this.setState(prevState => ({
count: prevState.count + 1
}));
}
State Updates Are Merged
In class components, setState() performs a shallow merge of the provided object with the current state:
// Component with multiple state properties
class UserForm extends Component {
constructor(props) {
super(props);
this.state = {
name: '',
email: '',
preferences: {
newsletter: false,
darkMode: false
}
};
}
updateName(name) {
// Only updates name, other state properties are preserved
this.setState({ name });
}
toggleNewsletter() {
// Nested objects aren't deeply merged - must include the entire nested object
this.setState({
preferences: {
...this.state.preferences,
newsletter: !this.state.preferences.newsletter
}
});
}
}
State in Functional Components with useState
In modern React, functional components use the useState hook to manage state. This approach is more concise and allows for better organization of state logic.
Basic useState Example
import React, { useState } from 'react';
function Counter() {
// useState returns a state value and a function to update it
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
return (
<div>
<h2>Count: {count}</h2>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
Multiple State Variables
Unlike class components where state is a single object, useState allows for separate state variables:
function UserForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [subscribed, setSubscribed] = useState(false);
return (
<form>
<div>
<label>Name:</label>
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
/>
</div>
<div>
<label>Email:</label>
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
/>
</div>
<div>
<label>
<input
type="checkbox"
checked={subscribed}
onChange={e => setSubscribed(e.target.checked)}
/>
Subscribe to newsletter
</label>
</div>
</form>
);
}
Using Objects with useState
For related state, you can still use objects with useState, but you need to manage updates differently:
function UserProfile() {
const [user, setUser] = useState({
name: '',
email: '',
preferences: {
newsletter: false,
darkMode: false
}
});
const updateName = (name) => {
// Must include all properties since useState doesn't merge
setUser({
...user, // spread the existing state
name // update the name property
});
};
const toggleNewsletter = () => {
// Nested objects require deeper spreading
setUser({
...user,
preferences: {
...user.preferences,
newsletter: !user.preferences.newsletter
}
});
};
}
Best Practices for useState
- Single Responsibility: Each state variable should ideally manage one aspect of state
- Group Related State: Use objects for closely related state, but consider splitting unrelated state
- Use Multiple useState Hooks: Don't be afraid to use many useState calls if it makes the code clearer
- Use Functional Updates: For state that depends on previous state, use the functional form
- Prefer Local State: Keep state as close as possible to where it's used
Functional Updates with useState
Just like with class components, when new state depends on the previous state, you should use the functional update form of the state setter:
Using Functional Updates
function Counter() {
const [count, setCount] = useState(0);
// Incorrect: May not use latest state due to closures
const increment = () => {
setCount(count + 1); // Uses value from render closure
};
// Correct: Always uses latest state
const betterIncrement = () => {
setCount(prevCount => prevCount + 1);
};
// This is particularly important with multiple updates
const incrementThree = () => {
// This might not increment by 3 because all updates
// reference the same closure value of count
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
// This will correctly increment by 3
const betterIncrementThree = () => {
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
};
return (
<div>
<h2>Count: {count}</h2>
<button onClick={betterIncrement}>+1</button>
<button onClick={betterIncrementThree}>+3</button>
</div>
);
}
Functional Updates with Object State
When working with object state, functional updates can be especially useful:
function TaskManager() {
const [tasks, setTasks] = useState([]);
const addTask = (newTask) => {
// Using functional update ensures we always have the latest state
setTasks(prevTasks => [...prevTasks, newTask]);
};
const removeTask = (taskId) => {
setTasks(prevTasks => prevTasks.filter(task => task.id !== taskId));
};
const updateTask = (taskId, updates) => {
setTasks(prevTasks =>
prevTasks.map(task =>
task.id === taskId ? { ...task, ...updates } : task
)
);
};
}
Real-World Example: Counter with Delay
Here's a practical example showing why functional updates are important:
function DelayedCounter() {
const [count, setCount] = useState(0);
// Problem: This might use stale state
const delayedIncrement = () => {
setTimeout(() => {
setCount(count + 1); // This uses the 'count' from when the function was created
}, 2000);
};
// Solution: Use functional update
const betterDelayedIncrement = () => {
setTimeout(() => {
setCount(prevCount => prevCount + 1); // This uses the latest state
}, 2000);
};
return (
<div>
<h2>Count: {count}</h2>
<button onClick={betterDelayedIncrement}>
Increment after 2 seconds
</button>
</div>
);
}
If you click the button multiple times in rapid succession, the functional update ensures each click is properly counted, whereas the non-functional version might miss updates.
Lazy Initialization of State
Both useState and class component state can use lazy initialization for expensive initial state calculations.
Lazy State Initialization
// Without lazy initialization
function UserPreferences() {
// This runs on every render
const [preferences, setPreferences] = useState(
JSON.parse(localStorage.getItem('preferences')) || {
theme: 'light',
fontSize: 'medium'
}
);
// ...
}
// With lazy initialization
function BetterUserPreferences() {
// The function only runs once during initial render
const [preferences, setPreferences] = useState(() => {
// Expensive operation only happens once
const savedPrefs = localStorage.getItem('preferences');
return savedPrefs
? JSON.parse(savedPrefs)
: { theme: 'light', fontSize: 'medium' };
});
// ...
}
When to Use Lazy Initialization
Use the lazy initialization pattern when:
- The initial state requires expensive computation
- The initial state is derived from localStorage or other storage
- The initial state requires complex transformations
- You want to avoid running the initialization logic on every render
State vs. Derived Values
Not everything needs to be in state. Values that can be calculated from existing state or props should not be duplicated in state.
Using Derived Values
// Bad: Redundant state
function CartWithRedundantState() {
const [items, setItems] = useState([
{ id: 1, name: 'Product 1', price: 10, quantity: 2 },
{ id: 2, name: 'Product 2', price: 15, quantity: 1 }
]);
const [itemCount, setItemCount] = useState(3); // Total quantity
const [totalPrice, setTotalPrice] = useState(35); // Total price
const addItem = (item) => {
setItems([...items, item]);
// Now we need to remember to update these derived values
setItemCount(itemCount + item.quantity);
setTotalPrice(totalPrice + (item.price * item.quantity));
};
// This approach is error-prone and can lead to inconsistencies
}
// Good: Calculate derived values when needed
function CartWithDerivedValues() {
const [items, setItems] = useState([
{ id: 1, name: 'Product 1', price: 10, quantity: 2 },
{ id: 2, name: 'Product 2', price: 15, quantity: 1 }
]);
// Calculate these values from the primary state
const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);
const totalPrice = items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
const addItem = (item) => {
setItems([...items, item]);
// No need to update derived values; they're recalculated on render
};
}
Optimizing Expensive Derived Calculations
If calculating derived values is expensive, you can use memoization with useMemo:
import { useState, useMemo } from 'react';
function ExpensiveCalculationExample() {
const [numbers, setNumbers] = useState([1, 2, 3, 4, 5]);
// This calculation will only re-run if the numbers array changes
const sum = useMemo(() => {
console.log('Calculating sum...');
return numbers.reduce((acc, num) => acc + num, 0);
}, [numbers]);
// This will re-calculate on every render (inefficient for expensive operations)
// const sum = numbers.reduce((acc, num) => acc + num, 0);
return (
<div>
<p>Sum: {sum}</p>
<button
onClick={() => setNumbers([...numbers, Math.floor(Math.random() * 100)])}
>
Add Random Number
</button>
</div>
);
}
When to Use State vs. Derived Values
- Use State For:
- Values that cannot be calculated from other state or props
- Values that need to trigger a re-render when changed
- Values that need to persist between renders
- Use Derived Values For:
- Values that can be calculated from existing state or props
- Transformed or filtered versions of existing data
- Aggregated values like counts, sums, or averages
State Management Patterns
As applications grow, managing state becomes more complex. Here are some common patterns for state management in React applications:
Lifting State Up
When multiple components need access to the same state, lift it to their closest common ancestor:
function ParentWithSharedState() {
// State is lifted to the parent component
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
return (
<div>
<h2>Count: {count}</h2>
{/* Both children work with the same state */}
<ChildA
count={count}
onIncrement={increment}
/>
<ChildB
count={count}
onDecrement={decrement}
/>
</div>
);
}
State Management Libraries
For larger applications, state management libraries like Redux, MobX, or Recoil can help manage complex state:
| Approach | Pros | Cons | Best For |
|---|---|---|---|
| Component State | Simple, built-in, easy to learn | Can lead to prop drilling, hard to share state | Small apps, component-specific state |
| Context API | Built-in, no dependencies, good for themes/auth | Performance concerns with frequent updates | Medium apps, infrequently changing state |
| Redux | Predictable, great dev tools, well-established | Boilerplate, steep learning curve | Large apps, complex state logic |
| MobX | Less boilerplate, automatic tracking | More "magic", less explicit data flow | Apps with complex domains, OOP fans |
| Recoil | Atomic model, great for code splitting | Newer, less mature ecosystem | Apps needing fine-grained reactivity |
| Zustand | Simple API, minimal boilerplate | Less established than Redux | Apps wanting Redux without the boilerplate |
Real-World Example: Todo App State Management
Let's see how component state can be structured for a small todo application:
function TodoApp() {
// Main state for the todo list
const [todos, setTodos] = useState([]);
// UI state for the new todo input
const [newTodoText, setNewTodoText] = useState('');
// UI state for filtering todos
const [filter, setFilter] = useState('all'); // 'all', 'active', 'completed'
// Derived value - filtered todos
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true; // 'all' filter
});
// Add new todo
const addTodo = () => {
if (!newTodoText.trim()) return;
const newTodo = {
id: Date.now(),
text: newTodoText,
completed: false
};
setTodos([...todos, newTodo]);
setNewTodoText(''); // Reset input
};
// Toggle todo completed status
const toggleTodo = (id) => {
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
// Delete todo
const deleteTodo = (id) => {
setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
};
// Clear all completed todos
const clearCompleted = () => {
setTodos(prevTodos => prevTodos.filter(todo => !todo.completed));
};
return (
<div className="todo-app">
{/* Input form */}
<form onSubmit={(e) => { e.preventDefault(); addTodo(); }}>
<input
type="text"
value={newTodoText}
onChange={(e) => setNewTodoText(e.target.value)}
placeholder="What needs to be done?"
/>
<button type="submit">Add</button>
</form>
{/* Todo list */}
<ul className="todo-list">
{filteredTodos.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)}>×</button>
</li>
))}
</ul>
{/* Filter controls */}
<div className="filters">
<button
className={filter === 'all' ? 'active' : ''}
onClick={() => setFilter('all')}
>
All
</button>
<button
className={filter === 'active' ? 'active' : ''}
onClick={() => setFilter('active')}
>
Active
</button>
<button
className={filter === 'completed' ? 'active' : ''}
onClick={() => setFilter('completed')}
>
Completed
</button>
{todos.some(todo => todo.completed) && (
<button onClick={clearCompleted}>Clear completed</button>
)}
</div>
{/* Todo count */}
<div className="todo-count">
{todos.filter(todo => !todo.completed).length} items left
</div>
</div>
);
}
For larger applications, you might split this into multiple components and use context or a state management library to share the todo state.
Practice Activities
Activity 1: Counter Application
Objective: Create a counter component with various features to practice state fundamentals.
Instructions:
- Create a counter component with increment and decrement buttons
- Add a reset button that returns the counter to its initial value
- Implement a "step" input that controls how much the counter changes with each click
- Add buttons for "Increment after 2 seconds" that uses functional updates
- Create a "count history" feature that tracks the last 5 values of the counter
Activity 2: Form State Management
Objective: Practice managing form state with useState.
Instructions:
- Create a sign-up form with the following fields:
- Name (text input)
- Email (email input)
- Password (password input)
- Confirm Password (password input)
- Role (select dropdown with options: Developer, Designer, Manager)
- Notifications (checkbox)
- Implement form validation:
- All fields except notifications are required
- Email must be a valid format
- Password must be at least 8 characters
- Confirm password must match password
- Display validation errors below each field
- Show a summary of the form data when submitted successfully
Activity 3: State Refactoring
Objective: Refactor inefficient state management in an existing component.
Instructions:
- Refactor the following component to use proper state management:
function ProductList() {
// Original product data
const [products, setProducts] = useState([
{ id: 1, name: 'Laptop', price: 999.99, inStock: true },
{ id: 2, name: 'Phone', price: 699.99, inStock: true },
{ id: 3, name: 'Tablet', price: 399.99, inStock: false }
]);
// Redundant state - these should be derived values
const [inStockProducts, setInStockProducts] = useState([]);
const [totalProducts, setTotalProducts] = useState(0);
const [totalValue, setTotalValue] = useState(0);
// Update derived state whenever products change
useEffect(() => {
const productsInStock = products.filter(p => p.inStock);
setInStockProducts(productsInStock);
setTotalProducts(products.length);
setTotalValue(products.reduce((sum, p) => sum + p.price, 0));
}, [products]);
// Toggles the stock status but has to manually update multiple state values
const toggleStock = (id) => {
const newProducts = products.map(p =>
p.id === id ? { ...p, inStock: !p.inStock } : p
);
setProducts(newProducts);
// Don't need these if using derived values
const productsInStock = newProducts.filter(p => p.inStock);
setInStockProducts(productsInStock);
setTotalProducts(newProducts.length);
setTotalValue(newProducts.reduce((sum, p) => sum + p.price, 0));
};
return (
<div>
<h2>Product List ({totalProducts} products, ${totalValue.toFixed(2)} total)</h2>
<ul>
{products.map(product => (
<li key={product.id}>
{product.name} - ${product.price.toFixed(2)}
<button onClick={() => toggleStock(product.id)}>
{product.inStock ? 'Mark Out of Stock' : 'Mark In Stock'}
</button>
</li>
))}
</ul>
<h3>In Stock Products ({inStockProducts.length})</h3>
<ul>
{inStockProducts.map(product => (
<li key={product.id}>
{product.name} - ${product.price.toFixed(2)}
</li>
))}
</ul>
</div>
);
}
Resources for Further Learning
Official Documentation:
Articles and Tutorials:
- Application State Management with React by Kent C. Dodds
- Managing State in React Docs Beta
- Choosing the State Structure in React Docs Beta
Summary
- State in React is essential for creating dynamic, interactive UIs that can respond to user input and other events
- Class components use
this.stateandthis.setState(), while functional components use theuseStatehook - State can be categorized in multiple ways: local vs. global, UI state vs. server state, etc.
- When updating state based on previous state, always use functional updates
- For expensive initial state calculations, use lazy initialization by passing a function to
useState - Avoid redundant state by using derived values for data that can be calculated from existing state
- As applications grow, consider using state management patterns like lifting state up or using a state management library
In the next lecture, we'll explore the useState hook in more depth, including advanced patterns and best practices.