Introduction to React-Redux
React-Redux is the official React binding library for Redux. It provides a clean interface for connecting your React components to the Redux store, enabling them to read data from the store and dispatch actions.
🌉 The Bridge Analogy
Think of React-Redux as a bridge between two islands:
- React Island: Your UI components
- Redux Island: Your state management
- React-Redux Bridge: The connection that allows data flow
- Provider: The foundation of the bridge
- Connect/Hooks: The lanes on the bridge
Without this bridge, the two islands would remain separate and unable to communicate effectively.
The Provider Component
The Provider component makes the Redux store available to any nested components that need to access the Redux state.
// Setting up the Provider
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import rootReducer from './reducers';
import App from './App';
// Create the Redux store
const store = createStore(rootReducer);
// Wrap your app with Provider
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
// With Redux DevTools
const store = createStore(
rootReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
// With middleware
import { applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
const store = createStore(
rootReducer,
applyMiddleware(thunk, logger)
);
Connecting Components - The Old Way
The connect() Function
Before hooks, we used the `connect()` function to connect components to Redux. While this pattern is still valid, hooks are now the recommended approach.
import React from 'react';
import { connect } from 'react-redux';
import { increment, decrement } from './actions';
// Presentational Component
const Counter = ({ count, increment, decrement }) => (
<div>
<h2>Count: {count}</h2>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
</div>
);
// mapStateToProps: Select data from store
const mapStateToProps = (state) => ({
count: state.counter.value
});
// mapDispatchToProps: Bind action creators
const mapDispatchToProps = {
increment,
decrement
};
// Alternative mapDispatchToProps as function
const mapDispatchToPropsFunc = (dispatch) => ({
increment: () => dispatch(increment()),
decrement: () => dispatch(decrement()),
incrementBy: (amount) => dispatch({ type: 'INCREMENT_BY', payload: amount })
});
// Connect component to Redux
export default connect(
mapStateToProps,
mapDispatchToProps
)(Counter);
// Using connect with options
export default connect(
mapStateToProps,
mapDispatchToProps,
null, // mergeProps
{
pure: true, // Enable pure component behavior
areStatesEqual: (next, prev) => next === prev,
areStatePropsEqual: (next, prev) =>
shallowEqual(next, prev)
}
)(Counter);
Modern Approach: React-Redux Hooks
useSelector Hook
The `useSelector` hook allows you to extract data from the Redux store state.
import React from 'react';
import { useSelector } from 'react-redux';
const TodoList = () => {
// Basic selector
const todos = useSelector(state => state.todos);
// Selector with computed value
const completedTodosCount = useSelector(state =>
state.todos.filter(todo => todo.completed).length
);
// Multiple selectors
const { user, isLoading } = useSelector(state => ({
user: state.auth.user,
isLoading: state.ui.isLoading
}));
// Selector with props
const TodoItem = ({ todoId }) => {
const todo = useSelector(state =>
state.todos.find(todo => todo.id === todoId)
);
return <div>{todo.text}</div>;
};
return (
<div>
<h2>Todos ({completedTodosCount} completed)</h2>
{todos.map(todo => (
<TodoItem key={todo.id} todoId={todo.id} />
))}
</div>
);
};
// Memoized selectors with reselect
import { createSelector } from 'reselect';
const selectTodos = state => state.todos;
const selectFilter = state => state.filter;
const selectVisibleTodos = createSelector(
[selectTodos, selectFilter],
(todos, filter) => {
switch (filter) {
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed);
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed);
default:
return todos;
}
}
);
const FilteredTodoList = () => {
const visibleTodos = useSelector(selectVisibleTodos);
// ...
};
useDispatch Hook
The `useDispatch` hook returns a reference to the dispatch function from the Redux store.
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { addTodo, toggleTodo } from './actions';
const TodoControls = () => {
const dispatch = useDispatch();
// Direct dispatch
const handleAddTodo = (text) => {
dispatch(addTodo(text));
};
// Memoized callback
const handleToggleTodo = useCallback((id) => {
dispatch(toggleTodo(id));
}, [dispatch]);
// Async dispatch with thunk
const handleFetchTodos = async () => {
dispatch({ type: 'FETCH_TODOS_START' });
try {
const response = await fetch('/api/todos');
const todos = await response.json();
dispatch({ type: 'FETCH_TODOS_SUCCESS', payload: todos });
} catch (error) {
dispatch({ type: 'FETCH_TODOS_FAILURE', payload: error.message });
}
};
return (
<div>
<button onClick={() => handleAddTodo('New Todo')}>
Add Todo
</button>
<button onClick={handleFetchTodos}>
Fetch Todos
</button>
</div>
);
};
useStore Hook
The `useStore` hook provides direct access to the Redux store instance.
import React from 'react';
import { useStore } from 'react-redux';
const StoreDebugger = () => {
const store = useStore();
const handleLogState = () => {
console.log('Current state:', store.getState());
};
return (
<button onClick={handleLogState}>
Log Current State
</button>
);
};
// Note: useStore is rarely needed in practice
// useSelector and useDispatch cover most use cases
Real-World Example: Shopping Cart
// Cart component using React-Redux hooks
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
addToCart,
removeFromCart,
updateQuantity,
applyDiscount
} from './cartActions';
const ShoppingCart = () => {
const [couponCode, setCouponCode] = useState('');
// Selectors
const cartItems = useSelector(state => state.cart.items);
const totalPrice = useSelector(state => state.cart.totalPrice);
const discount = useSelector(state => state.cart.discount);
const isLoading = useSelector(state => state.cart.isLoading);
// Computed values
const finalPrice = totalPrice - discount;
const itemCount = Object.values(cartItems).reduce(
(sum, item) => sum + item.quantity,
0
);
const dispatch = useDispatch();
// Event handlers
const handleQuantityChange = (productId, quantity) => {
dispatch(updateQuantity(productId, parseInt(quantity)));
};
const handleRemoveItem = (productId) => {
dispatch(removeFromCart(productId));
};
const handleApplyCoupon = () => {
dispatch(applyDiscount(couponCode));
};
const handleCheckout = async () => {
try {
// Async action
await dispatch(checkoutCart(cartItems));
} catch (error) {
console.error('Checkout failed:', error);
}
};
if (isLoading) {
return <div>Loading...</div>;
}
return (
<div className="shopping-cart">
<h2>Shopping Cart ({itemCount} items)</h2>
{Object.values(cartItems).map(item => (
<div key={item.product.id} className="cart-item">
<img src={item.product.image} alt={item.product.name} />
<h3>{item.product.name}</h3>
<p>${item.product.price}</p>
<select
value={item.quantity}
onChange={(e) => handleQuantityChange(
item.product.id,
e.target.value
)}
>
{[1,2,3,4,5].map(num => (
<option key={num} value={num}>{num}</option>
))}
</select>
<button onClick={() => handleRemoveItem(item.product.id)}>
Remove
</button>
</div>
))}
<div className="cart-summary">
<div className="coupon-section">
<input
type="text"
value={couponCode}
onChange={(e) => setCouponCode(e.target.value)}
placeholder="Enter coupon code"
/>
<button onClick={handleApplyCoupon}>Apply</button>
</div>
<div className="price-breakdown">
<p>Subtotal: ${totalPrice.toFixed(2)}</p>
{discount > 0 && <p>Discount: -${discount.toFixed(2)}</p>}
<h3>Total: ${finalPrice.toFixed(2)}</h3>
</div>
<button
onClick={handleCheckout}
disabled={itemCount === 0}
className="checkout-button"
>
Proceed to Checkout
</button>
</div>
</div>
);
};
// Product listing component
const ProductList = () => {
const products = useSelector(state => state.products.items);
const dispatch = useDispatch();
const handleAddToCart = (product) => {
dispatch(addToCart(product));
};
return (
<div className="product-list">
{products.map(product => (
<div key={product.id} className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={() => handleAddToCart(product)}>
Add to Cart
</button>
</div>
))}
</div>
);
};
Performance Optimization
Avoiding Unnecessary Re-renders
// Problem: Creating new object references
const BadComponent = () => {
// This creates a new object every render
const data = useSelector(state => ({
user: state.user,
posts: state.posts
}));
};
// Solution 1: Multiple useSelector calls
const GoodComponent = () => {
const user = useSelector(state => state.user);
const posts = useSelector(state => state.posts);
};
// Solution 2: Memoized selector
import { createSelector } from 'reselect';
const selectUserAndPosts = createSelector(
state => state.user,
state => state.posts,
(user, posts) => ({ user, posts })
);
const BetterComponent = () => {
const data = useSelector(selectUserAndPosts);
};
// Solution 3: Use shallowEqual
import { shallowEqual } from 'react-redux';
const AlsoGoodComponent = () => {
const data = useSelector(state => ({
user: state.user,
posts: state.posts
}), shallowEqual);
};
Optimizing Selectors
// Inefficient selector - runs on every state change
const ExpensiveComponent = () => {
const expensiveData = useSelector(state =>
state.items
.filter(item => item.active)
.map(item => ({ ...item, computed: calculateValue(item) }))
.sort((a, b) => b.computed - a.computed)
);
};
// Optimized with memoization
const selectActiveItems = state => state.items;
const selectExpensiveData = createSelector(
[selectActiveItems],
(items) => items
.filter(item => item.active)
.map(item => ({ ...item, computed: calculateValue(item) }))
.sort((a, b) => b.computed - a.computed)
);
const OptimizedComponent = () => {
const expensiveData = useSelector(selectExpensiveData);
};
// Parameterized selectors
const makeSelectItemById = () => createSelector(
[state => state.items, (state, id) => id],
(items, id) => items.find(item => item.id === id)
);
const ItemComponent = ({ itemId }) => {
const selectItemById = useMemo(makeSelectItemById, []);
const item = useSelector(state => selectItemById(state, itemId));
};
Common Patterns and Best Practices
Container/Presentational Pattern
// Container Component (connected to Redux)
const TodoListContainer = () => {
const todos = useSelector(state => state.todos);
const dispatch = useDispatch();
const handleToggle = (id) => {
dispatch(toggleTodo(id));
};
return <TodoList todos={todos} onToggle={handleToggle} />;
};
// Presentational Component (pure, no Redux)
const TodoList = ({ todos, onToggle }) => (
<ul>
{todos.map(todo => (
<li key={todo.id} onClick={() => onToggle(todo.id)}>
{todo.text}
</li>
))}
</ul>
);
Custom Hook Pattern
// Custom hook for auth logic
const useAuth = () => {
const user = useSelector(state => state.auth.user);
const isLoading = useSelector(state => state.auth.isLoading);
const error = useSelector(state => state.auth.error);
const dispatch = useDispatch();
const login = useCallback((credentials) => {
dispatch(loginUser(credentials));
}, [dispatch]);
const logout = useCallback(() => {
dispatch(logoutUser());
}, [dispatch]);
return { user, isLoading, error, login, logout };
};
// Usage in component
const LoginForm = () => {
const { user, isLoading, error, login } = useAuth();
const [credentials, setCredentials] = useState({
email: '',
password: ''
});
const handleSubmit = (e) => {
e.preventDefault();
login(credentials);
};
if (user) {
return <div>Welcome, {user.name}!</div>;
}
return (
<form onSubmit={handleSubmit}>
{/* form fields */}
</form>
);
};
Practice Exercise
Task: Build a Todo App with Redux
Create a Todo application that connects to Redux with the following features:
- Display a list of todos
- Add new todos
- Toggle todo completion
- Filter todos (all, active, completed)
- Show completion statistics
// Start with this basic structure
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
const TodoApp = () => {
// TODO: Implement selectors
const todos = useSelector(/* ... */);
const filter = useSelector(/* ... */);
// TODO: Get dispatch
const dispatch = useDispatch();
// TODO: Implement handlers
const handleAddTodo = (text) => {
// Dispatch add action
};
const handleToggleTodo = (id) => {
// Dispatch toggle action
};
const handleFilterChange = (filter) => {
// Dispatch filter action
};
// TODO: Create selector for visible todos
const visibleTodos = /* ... */;
return (
<div>
{/* TODO: Implement UI */}
</div>
);
};
// Bonus: Create custom hooks
const useTodos = () => {
// TODO: Encapsulate todo logic
};
const useTodoStats = () => {
// TODO: Calculate statistics
};