Introduction to React-Redux
While Redux can be used with any UI layer, it's most commonly paired with React. The official react-redux library provides bindings to connect Redux and React together efficiently.
The react-redux library provides several key components:
<Provider>: Makes the Redux store available to the entire React component treeconnect(): Higher-order component for connecting React components to Redux- Hooks:
useSelector()anduseDispatch()for using Redux with React hooks
// Installing react-redux
npm install react-redux
// or
yarn add react-redux
The Provider Component
The <Provider> component makes the Redux store available to all components in your application without passing it explicitly through props.
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 application with the Provider
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
The Provider uses React's Context API under the hood to make the store accessible to all components in the tree.
Real-World Analogy: Electric Grid
The Provider component is like the electric grid for a city:
- The power station (Redux store) generates electricity (state)
- The power grid (Provider) distributes electricity to the entire city
- Individual buildings (components) can tap into the grid without running direct lines to the power station
- Buildings don't need to know how electricity is generated, they just need to know how to connect to the grid
Just as you wouldn't want each building to have its own connection directly to the power station, you don't want each component to import and access the Redux store directly. The Provider creates a centralized distribution system.
The connect() Function
The connect() function is a higher-order component (HOC) that connects a React component to the Redux store.
Basic Structure
import { connect } from 'react-redux';
// Component definition
function MyComponent({ data, onButtonClick }) {
return (
<div>
<h2>{data.title}</h2>
<button onClick={onButtonClick}>Click Me</button>
</div>
);
}
// Map Redux state to component props
const mapStateToProps = (state, ownProps) => ({
data: state.someData
});
// Map Redux actions to component props
const mapDispatchToProps = (dispatch, ownProps) => ({
onButtonClick: () => dispatch({ type: 'SOME_ACTION' })
});
// Connect component to Redux
export default connect(
mapStateToProps,
mapDispatchToProps
)(MyComponent);
mapStateToProps
The mapStateToProps function determines which parts of the Redux state are exposed to your component as props.
// Basic mapStateToProps
const mapStateToProps = state => ({
todos: state.todos,
filter: state.visibilityFilter
});
// Using selectors in mapStateToProps
import { getVisibleTodos } from './selectors';
const mapStateToProps = state => ({
todos: getVisibleTodos(state),
filter: state.visibilityFilter
});
// Using component's own props (ownProps)
const mapStateToProps = (state, ownProps) => ({
todo: state.todos.find(todo => todo.id === ownProps.todoId)
});
Best practices for mapStateToProps:
- Use selectors to extract and transform data
- Keep components decoupled from the state shape
- Only select the data the component needs
- Consider memoization for expensive calculations
mapDispatchToProps
The mapDispatchToProps function lets you create callback props that dispatch actions when called.
// Object shorthand
const mapDispatchToProps = {
addTodo: text => ({ type: 'ADD_TODO', payload: { text } }),
toggleTodo: id => ({ type: 'TOGGLE_TODO', payload: { id } })
};
// Function form
const mapDispatchToProps = dispatch => ({
addTodo: text => dispatch({ type: 'ADD_TODO', payload: { text } }),
toggleTodo: id => dispatch({ type: 'TOGGLE_TODO', payload: { id } })
});
// Using action creators
import { addTodo, toggleTodo } from './actions';
// Object shorthand with action creators
const mapDispatchToProps = {
addTodo,
toggleTodo
};
// Function form with action creators
const mapDispatchToProps = dispatch => ({
addTodo: text => dispatch(addTodo(text)),
toggleTodo: id => dispatch(toggleTodo(id))
});
Best practices for mapDispatchToProps:
- Use action creators instead of inline action objects
- Use the object shorthand form when possible
- Name props based on the event (e.g.,
onSubmit) rather than the action (e.g.,dispatchAddTodo) - Encapsulate complex dispatch logic in action creators, not in
mapDispatchToProps
Real-World Example: E-commerce Product Page
Here's how Redux might connect to a product detail page in an e-commerce application:
import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import { fetchProduct, addToCart } from './actions';
import LoadingSpinner from './LoadingSpinner';
import ErrorMessage from './ErrorMessage';
function ProductDetail({
product,
loading,
error,
fetchProduct,
addToCart,
match // from react-router
}) {
const productId = match.params.id;
useEffect(() => {
fetchProduct(productId);
}, [productId, fetchProduct]);
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage message={error} />;
if (!product) return <div>Product not found</div>;
return (
<div className="product-detail">
<h1>{product.name}</h1>
<div className="product-image">
<img src={product.imageUrl} alt={product.name} />
</div>
<div className="product-info">
<p className="price">${product.price.toFixed(2)}</p>
<p className="description">{product.description}</p>
<button
onClick={() => addToCart(product.id)}
disabled={!product.inStock}
>
{product.inStock ? 'Add to Cart' : 'Out of Stock'}
</button>
</div>
</div>
);
}
const mapStateToProps = (state, ownProps) => {
const productId = ownProps.match.params.id;
return {
product: state.products.items[productId],
loading: state.products.loading,
error: state.products.error
};
};
const mapDispatchToProps = {
fetchProduct,
addToCart
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(ProductDetail);
This example shows how a real-world component would connect to Redux to fetch product data and dispatch actions like adding to cart.
React-Redux Hooks
In modern React applications with hooks, react-redux provides hook alternatives to connect().
useSelector
The useSelector hook lets you extract data from the Redux store state.
import React from 'react';
import { useSelector } from 'react-redux';
function TodoList() {
// Extract data from Redux store
const todos = useSelector(state => state.todos);
const filter = useSelector(state => state.visibilityFilter);
// Filter the todos based on the filter
const visibleTodos = todos.filter(todo => {
if (filter === 'SHOW_COMPLETED') return todo.completed;
if (filter === 'SHOW_ACTIVE') return !todo.completed;
return true; // SHOW_ALL
});
return (
<ul>
{visibleTodos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
Key features of useSelector:
- Selector function runs whenever the component renders
- Component re-renders when the selector result changes
- Uses strict
===comparison by default - Can specify a custom equality function
// With custom equality function
import { useSelector, shallowEqual } from 'react-redux';
function TodoList() {
// Only re-render if the array reference changes or its contents change
const todos = useSelector(state => state.todos, shallowEqual);
// Rest of component...
}
useDispatch
The useDispatch hook gives you access to the Redux store's dispatch function.
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { addTodo } from './actions';
function AddTodo() {
const [text, setText] = useState('');
const dispatch = useDispatch();
const handleSubmit = e => {
e.preventDefault();
if (!text.trim()) return;
dispatch(addTodo(text));
setText('');
};
return (
<form onSubmit={handleSubmit}>
<input
value={text}
onChange={e => setText(e.target.value)}
placeholder="Add a todo"
/>
<button type="submit">Add</button>
</form>
);
}
useStore
The useStore hook returns a reference to the same Redux store that was passed to the <Provider>.
import { useStore } from 'react-redux';
function ComponentWithStoreAccess() {
const store = useStore();
// This is generally not recommended, but can be useful in rare cases
const state = store.getState();
return (
<div>
Store accessed directly: {state.someValue}
</div>
);
}
Hooks vs connect(): When to Use Each
Both approaches have their strengths:
| Hooks API | connect() API |
|---|---|
| More modern, fits with functional components | Works with class components |
| Less boilerplate code | More explicit about data dependencies |
| More flexible for custom logic | Better performance optimizations out of the box |
| Easier to understand for developers familiar with hooks | More established patterns and best practices |
In modern applications, hooks are generally preferred for new code, but both approaches are valid and can even be mixed within the same application.
Performance Optimizations
Connecting Redux to React efficiently requires careful attention to performance to avoid unnecessary re-renders.
Selector Memoization
Use the reselect library to create memoized selectors that only recalculate when their inputs change.
import { createSelector } from 'reselect';
import { useSelector } from 'react-redux';
// Input selectors
const getTodos = state => state.todos;
const getFilter = state => state.visibilityFilter;
// Memoized selector
const getVisibleTodos = createSelector(
[getTodos, getFilter],
(todos, filter) => {
switch (filter) {
case 'SHOW_COMPLETED':
return todos.filter(todo => todo.completed);
case 'SHOW_ACTIVE':
return todos.filter(todo => !todo.completed);
default:
return todos;
}
}
);
// Component using the memoized selector
function TodoList() {
// Only recalculates when todos or filter changes
const visibleTodos = useSelector(getVisibleTodos);
return (
<ul>
{visibleTodos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
React.memo
Wrap your connected components with React.memo to prevent re-renders when props haven't changed.
import React, { memo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { toggleTodo } from './actions';
// Memoized component only re-renders when its props change
const TodoItem = memo(function TodoItem({ todo }) {
const dispatch = useDispatch();
return (
<li
onClick={() => dispatch(toggleTodo(todo.id))}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
>
{todo.text}
</li>
);
});
function TodoList() {
const todos = useSelector(state => state.todos);
return (
<ul>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
}
Equality Comparisons
Customize equality checks in useSelector to prevent unnecessary re-renders.
import { useSelector, shallowEqual } from 'react-redux';
function MyComponent() {
// Using shallow equality check
const { name, age } = useSelector(
state => ({
name: state.user.name,
age: state.user.age
}),
shallowEqual // Prevents re-renders if name and age haven't changed
);
return (
<div>
Name: {name}, Age: {age}
</div>
);
}
Batch Updates
Redux dispatches that happen during React event handlers are automatically batched, but for other scenarios, you might need to batch manually.
import { batch } from 'react-redux';
function handleComplexOperation() {
batch(() => {
// All of these dispatches will be batched into one render update
dispatch(action1());
dispatch(action2());
dispatch(action3());
});
}
Performance Optimization Checklist
When connecting Redux to React, follow this checklist for optimal performance:
- Normalize state to avoid deep nested objects
- Use selectors for computing derived data
- Memoize selectors with reselect for expensive calculations
- Connect components at the right level - not too high, not too low in the component tree
- Use React.memo for connected components
- Consider shallowEqual for complex object comparisons
- Batch multiple dispatches when making several updates at once
- Avoid creating new objects or arrays in mapStateToProps or useSelector
- Use DevTools to identify and fix performance bottlenecks
Container and Presentational Components
A common pattern when using Redux with React is to separate "container" components (connected to Redux) from "presentational" components (pure UI).
Container Components
- Connected to Redux via connect() or hooks
- Responsible for how things work (data fetching, state updates)
- Usually don't have their own markup or styles
- Pass data and callbacks to presentational components
Presentational Components
- Not connected to Redux, receive everything via props
- Responsible for how things look
- Contain markup and styles
- Often reusable and have no dependencies on Redux
// TodoListContainer.js - Container Component
import { connect } from 'react-redux';
import { toggleTodo } from '../actions';
import { getVisibleTodos } from '../selectors';
import TodoList from '../components/TodoList';
const mapStateToProps = state => ({
todos: getVisibleTodos(state)
});
const mapDispatchToProps = {
onTodoClick: toggleTodo
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(TodoList);
// TodoList.js - Presentational Component
import React from 'react';
import TodoItem from './TodoItem';
function TodoList({ todos, onTodoClick }) {
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
{...todo}
onClick={() => onTodoClick(todo.id)}
/>
))}
</ul>
);
}
export default TodoList;
Benefits of this separation:
- Better reusability of presentational components
- Clearer separation of concerns
- Easier to test presentational components in isolation
- Can replace Redux with another state management solution without changing UI
Real-World Analogy: Restaurant Organization
The container/presentational pattern is like the division of labor in a restaurant:
- Kitchen (Redux Store): Where the food (state) is prepared and stored
- Waitstaff (Container Components): Carry food from the kitchen to the tables, know the menu, take orders back to the kitchen
- Dining Room (Presentational Components): Where customers interact with the food, tables arranged for optimal experience
Just as waitstaff don't cook the food and chefs don't interact directly with customers, containers handle Redux logic while presentational components handle UI without knowing about Redux.
Connected Components with TypeScript
When using TypeScript with Redux and React, you can add type safety to your connected components and hooks.
Typing connect()
import { connect, ConnectedProps } from 'react-redux';
import { RootState } from '../store';
import { toggleTodo } from '../actions';
// Define mapStateToProps with types
const mapStateToProps = (state: RootState) => ({
todos: state.todos
});
// Define mapDispatchToProps
const mapDispatchToProps = {
toggleTodo
};
// Generate type-safe props using ConnectedProps
const connector = connect(mapStateToProps, mapDispatchToProps);
type PropsFromRedux = ConnectedProps<typeof connector>;
// Component with props typing
interface TodoListProps extends PropsFromRedux {
title: string; // Additional props not from Redux
}
function TodoList({ todos, toggleTodo, title }: TodoListProps) {
return (
<div>
<h2>{title}</h2>
<ul>
{todos.map(todo => (
<li
key={todo.id}
onClick={() => toggleTodo(todo.id)}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
>
{todo.text}
</li>
))}
</ul>
</div>
);
}
// Connect component to Redux
export default connector(TodoList);
Typing Hooks
import { useSelector, useDispatch } from 'react-redux';
import { RootState, AppDispatch } from '../store';
import { toggleTodo } from '../actions';
// Define typed hooks
export const useAppSelector = <T>(selector: (state: RootState) => T) =>
useSelector<RootState, T>(selector);
export const useAppDispatch = () => useDispatch<AppDispatch>();
// Component with typed hooks
interface TodoListProps {
title: string;
}
function TodoList({ title }: TodoListProps) {
// Use typed selector
const todos = useAppSelector(state => state.todos);
// Use typed dispatch
const dispatch = useAppDispatch();
return (
<div>
<h2>{title}</h2>
<ul>
{todos.map(todo => (
<li
key={todo.id}
onClick={() => dispatch(toggleTodo(todo.id))}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
>
{todo.text}
</li>
))}
</ul>
</div>
);
}
export default TodoList;
Benefits of type safety:
- Catch errors at compile time instead of runtime
- Better editor autocomplete and intellisense
- Self-documenting code that's easier to understand
- Safer refactoring when changing state shape or action structure
Testing Connected Components
Testing components connected to Redux requires some special considerations.
Testing Presentational Components
Presentational components are easy to test as they're just pure functions of their props.
// TodoList.test.js
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import TodoList from './TodoList';
test('renders todos and handles clicks', () => {
const todos = [
{ id: 1, text: 'Test Todo', completed: false }
];
const toggleTodo = jest.fn();
const { getByText } = render(
<TodoList todos={todos} toggleTodo={toggleTodo} />
);
const todoElement = getByText('Test Todo');
expect(todoElement).toBeInTheDocument();
fireEvent.click(todoElement);
expect(toggleTodo).toHaveBeenCalledWith(1);
});
Testing Connected Components
For connected components, you have several options:
1. Mocking the Redux Store
// TodoList.test.js (for connected component)
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import ConnectedTodoList from './ConnectedTodoList';
import { toggleTodo } from '../actions';
const mockStore = configureStore([]);
test('connected component dispatches actions', () => {
const initialState = {
todos: [
{ id: 1, text: 'Test Todo', completed: false }
]
};
const store = mockStore(initialState);
const { getByText } = render(
<Provider store={store}>
<ConnectedTodoList />
</Provider>
);
fireEvent.click(getByText('Test Todo'));
// Check that the correct action was dispatched
expect(store.getActions()).toEqual([
toggleTodo(1)
]);
});
2. Testing the Component in Isolation
// Export the unconnected component for testing
export function TodoList({ todos, toggleTodo }) {
// Component implementation
}
// Connect and export the connected component
export default connect(
mapStateToProps,
mapDispatchToProps
)(TodoList);
// In your test file
import { TodoList } from './TodoList'; // Import the unconnected component
test('unconnected component', () => {
// Test without Redux, just like a presentational component
});
3. Using a Real Redux Store
// Using a real Redux store for integration testing
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import rootReducer from '../reducers';
import ConnectedTodoList from './ConnectedTodoList';
test('connected component with real store', () => {
const store = createStore(rootReducer, {
todos: [
{ id: 1, text: 'Test Todo', completed: false }
]
});
const { getByText } = render(
<Provider store={store}>
<ConnectedTodoList />
</Provider>
);
// Initial state
expect(getByText('Test Todo')).toHaveStyle('text-decoration: none');
// Click to toggle
fireEvent.click(getByText('Test Todo'));
// Check that the state was updated through the reducer
expect(getByText('Test Todo')).toHaveStyle('text-decoration: line-through');
});
Practice Activities
Activity 1: Connecting a Todo App to Redux
Build a simple todo application with the following components:
- A form for adding new todos
- A list of todos that can be toggled between completed and incomplete
- Filter buttons to show all, completed, or active todos
Connect each component to Redux using both methods:
- First implementation: Use
connect()with mapStateToProps and mapDispatchToProps - Second implementation: Refactor to use
useSelectoranduseDispatchhooks
Activity 2: Container and Presentational Pattern
Refactor the todo app from Activity 1 to use the container/presentational pattern:
- Create presentational components that receive all data via props
- Create container components that connect to Redux and pass data to presentational components
- Ensure the presentational components have no Redux dependencies
- Write tests for both container and presentational components
Activity 3: Performance Optimization
Optimize the todo app for performance:
- Implement memoized selectors with reselect
- Use React.memo for appropriate components
- Apply shallowEqual for object comparisons in useSelector
- Batch updates when dispatching multiple actions
- Measure performance before and after optimizations
Summary
- React-Redux provides official bindings to connect Redux and React
- The
<Provider>component makes the store available to your entire app connect()is a higher-order component that connects React components to ReduxmapStateToPropsdetermines which parts of the state are exposed to componentsmapDispatchToPropscreates callback props that dispatch actions- React-Redux hooks (
useSelector,useDispatch) provide a simpler way to connect to Redux - Performance optimizations include memoized selectors, React.memo, and custom equality checks
- The container/presentational pattern separates Redux logic from UI components
- TypeScript adds type safety to connected components and hooks
- Testing connected components requires special techniques like mocking the store