React’s component-based architecture revolves around the concept of state. Understanding how to effectively initialize, update, and manage state is crucial for building robust and performant React applications. In this comprehensive guide, we’ll explore various methods of state management in React, focusing on functional components and hooks. We’ll cover everything from basic state initialization to advanced state management techniques, including the batching process.
useState(): The Building Block of State Management
The useState
hook is the most basic and commonly used method for managing the state in functional components. It’s perfect for simple state management scenarios.
const [state, setState] = useState(initialState);
Shopping Cart Item Counter Example:
Let’s create a simple shopping cart item counter using useState
:
import React, { useState } from 'react';
function ShoppingCartCounter() {
const [itemCount, setItemCount] = useState(0);
const addItem = () => setItemCount(prevCount => prevCount + 1);
const removeItem = () => setItemCount(prevCount => Math.max(0, prevCount - 1));
return (
<div>
<h2>Shopping Cart</h2>
<p>Items in cart: {itemCount}</p>
<button onClick={addItem}>Add Item</button>
<button onClick={removeItem}>Remove Item</button>
</div>
);
}
In this example, we use useState
to initialize and manage the itemCount state. The setItemCount function allows us to update the state based on user interactions.
useReducer()
: Managing Complex State Logic
When state logic becomes more complex or when the next state depends on the previous one, useReducer
can be a more suitable choice.
Todo List Example:
Let’s create a more complex todo list application using useReducer:
import React, { useReducer } from 'react';
// Reducer function
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, { id: Date.now(), text: action.payload, completed: false }];
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
);
case 'DELETE_TODO':
return state.filter(todo => todo.id !== action.payload);
default:
return state;
}
}
function TodoList() {
const [todos, dispatch] = useReducer(todoReducer, []);
const [inputText, setInputText] = React.useState('');
const addTodo = () => {
if (inputText.trim()) {
dispatch({ type: 'ADD_TODO', payload: inputText });
setInputText('');
}
};
return (
<div>
<h2>Todo List</h2>
<input
value={inputText}
onChange={(e) => setInputText(e.target.value)}
placeholder="Enter a new todo"
/>
<button onClick={addTodo}>Add Todo</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<span
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
onClick={() => dispatch({ type: 'TOGGLE_TODO', payload: todo.id })}
>
{todo.text}
</span>
<button onClick={() => dispatch({ type: 'DELETE_TODO', payload: todo.id })}>
Delete
</button>
</li>
))}
</ul>
</div>
);
}
useContext()
: Sharing State Across Components
The useContext
hook, combined with the Context API, allows you to share state across multiple components without explicitly passing props through every level of the component tree.
Theme Switcher Example:
import React, { createContext, useContext, useState } from 'react';
// Create a context
const ThemeContext = createContext();
// Theme provider component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// A component that uses the theme
function ThemedButton() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button
onClick={toggleTheme}
style={{
backgroundColor: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff',
}}
>
Toggle Theme
</button>
);
}
// Another component that uses the theme
function ThemedText() {
const { theme } = useContext(ThemeContext);
return (
<p style={{ color: theme === 'light' ? '#333' : '#fff' }}>
Current theme: {theme}
</p>
);
}
// Main app component
function App() {
return (
<ThemeProvider>
<div>
<h1>Theme Switcher Example</h1>
<ThemedText />
<ThemedButton />
</div>
</ThemeProvider>
);
}
Redux Toolkit: Centralized State Management:
For larger applications with complex state management needs, Redux Toolkit offers a powerful solution. It provides utilities to simplify common Redux use cases, including store setup, defining reducers, immutable update logic, and even creating entire “slices” of state at once.
E-commerce Cart Management:
import React from 'react';
import { configureStore, createSlice } from '@reduxjs/toolkit';
import { Provider, useSelector, useDispatch } from 'react-redux';
// Create a slice for cart state
const cartSlice = createSlice({
name: 'cart',
initialState: [],
reducers: {
addItem: (state, action) => {
const existingItem = state.find(item => item.id === action.payload.id);
if (existingItem) {
existingItem.quantity += 1;
} else {
state.push({ ...action.payload, quantity: 1 });
}
},
removeItem: (state, action) => {
const index = state.findIndex(item => item.id === action.payload);
if (index !== -1) {
if (state[index].quantity > 1) {
state[index].quantity -= 1;
} else {
state.splice(index, 1);
}
}
},
},
});
// Create the store
const store = configureStore({
reducer: {
cart: cartSlice.reducer,
},
});
// Extract action creators
const { addItem, removeItem } = cartSlice.actions;
// Cart component
function Cart() {
const cartItems = useSelector(state => state.cart);
const dispatch = useDispatch();
return (
<div>
<h2>Shopping Cart</h2>
{cartItems.map(item => (
<div key={item.id}>
<span>{item.name} - Quantity: {item.quantity}</span>
<button onClick={() => dispatch(addItem(item))}>+</button>
<button onClick={() => dispatch(removeItem(item.id))}>-</button>
</div>
))}
</div>
);
}
// Product list component
function ProductList() {
const dispatch = useDispatch();
const products = [
{ id: 1, name: 'Product 1', price: 10 },
{ id: 2, name: 'Product 2', price: 15 },
{ id: 3, name: 'Product 3', price: 20 },
];
return (
<div>
<h2>Products</h2>
{products.map(product => (
<div key={product.id}>
<span>{product.name} - ${product.price}</span>
<button onClick={() => dispatch(addItem(product))}>Add to Cart</button>
</div>
))}
</div>
);
}
// Main app component
function App() {
return (
<Provider store={store}>
<div>
<h1>E-commerce App</h1>
<ProductList />
<Cart />
</div>
</Provider>
);
}
Understanding the Batching Process
Before we dive into the batching process, let’s understand the problem it solves. In React, state updates trigger re-renders of components. When multiple state updates occur in rapid succession, it can lead to unnecessary re-renders, affecting performance.
Multiple Re-renders Example:
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
console.log('Render');
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
In this example, you might expect the count to increase by 3 when the button is clicked. However, it only increases by 1. This is because React batches the state updates that occur in event handlers. The console.log('Render') would only appear once in the console, indicating a single re-render.
The Solution: Automatic Batching
React 18 introduced automatic batching for all updates, regardless of where they originate. This means that multiple state updates are grouped together, resulting in a single re-render.
Here’s how you can leverage batching for better performance:
function Counter() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
const handleClick = () => {
setCount(c => c + 1);
setFlag(f => !f);
// These updates will be batched together
};
console.log('Render');
return (
<div>
<p>Count: {count}</p>
<p>Flag: {flag.toString()}</p>
<button onClick={handleClick}>Update</button>
</div>
);
}
The article is originally posted at Programmingly.dev, read the full article at following link
https://programmingly.dev/managing-state-in-react-using-different-method-understand-batching