🔌 Connecting Redux to React
Redux by itself is UI-agnostic — it's just a store and some rules. The official react-redux library is the bridge that lets your components read Redux state and dispatch actions efficiently. This lesson focuses on the modern hooks approach you'll actually use, then covers performance and testing.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Wrap an app with
<Provider>so every component can reach the store - Read state with
useSelectorand send actions withuseDispatch - Explain how the modern hooks relate to the older
connect()HOC - Prevent unnecessary re-renders with memoized selectors,
React.memo, and equality functions - Separate container and presentational concerns and test connected components
Estimated Time: 35–45 minutes • Difficulty: Intermediate
Hands-on: Build a fully wired todo app — Provider, hooks, add/toggle/filter — from scratch.
In This Lesson
The react-redux Bridge
Redux can drive any UI. To pair it with React efficiently you use react-redux, the official binding library maintained by the Redux team. It solves two problems: making the single store reachable from anywhere in the tree, and re-rendering a component only when the exact slice of state it reads has changed.
You install it alongside Redux Toolkit:
npm install @reduxjs/toolkit react-redux
It gives you three things you'll use daily:
<Provider>— makes the store available to the whole component tree.useSelector()— reads (and subscribes to) a slice of state.useDispatch()— returns the store'sdispatchfunction.
📖 Hooks first
Modern React apps use the hooks (useSelector/useDispatch) for essentially all new code. The older connect() higher-order component still works and appears in many codebases, so we cover it — but treat the hooks as your default.
The Provider Component
<Provider> wraps your app once, at the very top, and passes the store down through React's Context under the hood. After that, any descendant can reach the store via the hooks — no manual prop passing.
// main.jsx — React 18/19 entry point
import React from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './store'; // created with configureStore
import App from './App';
createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
⚠️ Modern React entry point
Note the API: React 18 and 19 mount with createRoot(...).render(...) from react-dom/client. The old ReactDOM.render() you may see in older tutorials is removed in React 19 — don't use it.
⚡ Electric-grid analogy: The store is a power station; <Provider> is the grid that distributes power to the whole city. Individual buildings (components) tap into the grid wherever they are, without running their own line back to the station. They don't need to know how the power is generated — only how to plug in.
useSelector & useDispatch
Reading state with useSelector
useSelector takes a selector function, runs it against the current state, and returns the result. It also subscribes the component: whenever the store updates, it re-runs the selector, and if the returned value changed (by === reference comparison), it re-renders.
import { useSelector } from 'react-redux';
function TodoList() {
// Select just what this component needs
const todos = useSelector(state => state.todos.items);
const filter = useSelector(state => state.todos.filter);
const visible = todos.filter(t =>
filter === 'completed' ? t.completed :
filter === 'active' ? !t.completed : true
);
return (
<ul>
{visible.map(todo => <li key={todo.id}>{todo.text}</li>)}
</ul>
);
}
⚠️ The new-object trap
If a selector returns a new object or array every time — e.g. useSelector(state => ({ a: state.a, b: state.b })) — the === check always fails and the component re-renders on every store update. Fix it by selecting primitives separately, using a memoized selector, or passing shallowEqual as the second argument.
import { useSelector, shallowEqual } from 'react-redux';
// shallowEqual compares each field, so a new wrapper object with the
// same contents does NOT trigger a re-render
const { name, age } = useSelector(
state => ({ name: state.user.name, age: state.user.age }),
shallowEqual
);
Dispatching with useDispatch
useDispatch returns the store's dispatch function. Call it with an action (usually from an action creator) to trigger a change.
import { useState } from 'react';
import { useDispatch } from 'react-redux';
import { addTodo } from './todosSlice'; // action creator from createSlice
function AddTodo() {
const [text, setText] = useState('');
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
if (!text.trim()) return;
dispatch(addTodo(text)); // send the action
setText('');
};
return (
<form onSubmit={handleSubmit}>
<input value={text} onChange={(e) => setText(e.target.value)} placeholder="Add a todo" />
<button type="submit">Add</button>
</form>
);
}
✅ Select narrowly, dispatch freely
Two habits keep hook-based Redux fast and clean: select the smallest piece of state each component actually needs (narrow selectors mean fewer re-renders), and dispatch action creators rather than hand-built action objects (readable, reusable, typo-proof).
The Legacy connect() API
Before hooks, components connected to Redux through the connect() higher-order component. You'll still meet it in existing code and class components, so it's worth being able to read.
import { connect } from 'react-redux';
import { toggleTodo } from './actions';
function TodoList({ todos, toggleTodo }) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id} onClick={() => toggleTodo(todo.id)}>{todo.text}</li>
))}
</ul>
);
}
// Which slice of state becomes props:
const mapStateToProps = (state) => ({ todos: state.todos.items });
// Which dispatching callbacks become props (object shorthand):
const mapDispatchToProps = { toggleTodo };
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
Hooks (useSelector/useDispatch) | connect() HOC |
|---|---|
| Default for new function-component code | Needed for class components |
| Less boilerplate, reads top-to-bottom | Explicit map functions, more ceremony |
| Data dependencies live inline in the component | Data dependencies declared separately |
| Recommended going forward | Fully supported but legacy |
🍽️ Restaurant analogy: Whether you use hooks or connect(), the split is the same: the kitchen (store) prepares state, the waitstaff (the connecting layer) carry it to and from the tables, and the dining room (your presentational UI) never walks into the kitchen. Hooks are just a newer, lighter waitstaff.
Performance Optimizations
Connecting Redux to React well is mostly about not re-rendering when nothing you care about changed. Four tools cover almost every case.
1. Memoized selectors
Use createSelector for derived data so filtering/mapping only reruns when its inputs change — and it returns the same array reference otherwise, which prevents downstream re-renders.
import { createSelector } from '@reduxjs/toolkit';
import { useSelector } from 'react-redux';
const selectItems = state => state.todos.items;
const selectFilter = state => state.todos.filter;
const selectVisibleTodos = createSelector(
[selectItems, selectFilter],
(items, filter) =>
filter === 'completed' ? items.filter(t => t.completed) :
filter === 'active' ? items.filter(t => !t.completed) : items
);
function TodoList() {
const visible = useSelector(selectVisibleTodos); // recomputes only when needed
return <ul>{visible.map(t => <li key={t.id}>{t.text}</li>)}</ul>;
}
2. React.memo
Wrap a child component in React.memo so it only re-renders when its props change, even if a parent re-renders.
import { memo } from 'react';
import { useDispatch } from 'react-redux';
import { toggleTodo } from './todosSlice';
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>
);
});
3. Equality functions
Pass shallowEqual (or React-Redux's useSelector with a custom comparator) when a selector must return an object, so equal contents don't count as a change.
4. Connect at the right level
Have each list item read its own data rather than the parent passing a giant array down. Fine-grained selection means a change to one todo re-renders one item, not the whole list.
💡 Performance checklist
- Normalize state to avoid deep nesting.
- Select narrowly — the smallest slice each component needs.
- Memoize derived data with
createSelector. - Avoid new objects/arrays inside
useSelector(or useshallowEqual). React.memopure child components.- Use Redux DevTools and React's Profiler to find the actual hotspots before optimizing.
📖 Note: automatic batching
React 18+ automatically batches multiple state updates — including several dispatches inside one event handler, a promise, or a timeout — into a single re-render. The old manual batch() helper from react-redux is no longer needed in modern React.
Container & Presentational Components, and Testing
A durable pattern is to keep presentational components (pure UI, everything via props, no Redux imports) separate from the connecting logic (hooks or containers that read state and dispatch). Presentational components are reusable and trivial to test.
(props only, no Redux)"]
Because presentational components are pure functions of props, you test them with no store at all:
import { render, screen, fireEvent } from '@testing-library/react';
import { TodoList } from './TodoList'; // the unconnected, presentational version
test('renders todos and reports clicks', () => {
const todos = [{ id: 1, text: 'Test Todo', completed: false }];
const onToggle = jest.fn();
render(<TodoList todos={todos} onToggle={onToggle} />);
fireEvent.click(screen.getByText('Test Todo'));
expect(onToggle).toHaveBeenCalledWith(1);
});
For a component that uses the hooks, the recommended approach is an integration test with a real store, rendered inside a <Provider>:
import { render, screen, fireEvent } from '@testing-library/react';
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import todosReducer from './todosSlice';
import TodoList from './TodoList';
test('toggling a todo updates the store and the UI', () => {
const store = configureStore({
reducer: { todos: todosReducer },
preloadedState: { todos: { items: [{ id: 1, text: 'Test Todo', completed: false }], filter: 'all' } }
});
render(
<Provider store={store}>
<TodoList />
</Provider>
);
expect(screen.getByText('Test Todo')).toHaveStyle('text-decoration: none');
fireEvent.click(screen.getByText('Test Todo'));
expect(screen.getByText('Test Todo')).toHaveStyle('text-decoration: line-through');
});
✅ The current best practice
The Redux team recommends testing with a real store and Testing Library rather than mocking the store or shallow-rendering. You test behavior a user can observe — "clicking toggles the line-through" — which survives refactors far better than asserting on internal action lists.
Hands-on: Wire a Todo App
🏋️ Connect a real feature end to end
Objective: Assemble everything — a Toolkit slice, a Provider, and hook-connected components — into a working todo app with add, toggle, and filter.
Instructions:
- Create a
todosSlicewithcreateSlice: state{ items: [], filter: 'all' }and reducersaddTodo,toggleTodo,setFilter. - Build the store with
configureStoreand wrap<App />in<Provider>. - Write an
AddTodoform (useDispatch) and aTodoList(useSelectorwith a memoized visible-todos selector) whose items toggle on click.
💡 Hint
createSlice auto-generates the action creators from your reducer names — export them (export const { addTodo, toggleTodo, setFilter } = todosSlice.actions). Inside slice reducers you can write "mutating" code like state.items.push(...) because Immer makes it immutable for you.
✅ Solution
// todosSlice.js
import { createSlice, createSelector } from '@reduxjs/toolkit';
const todosSlice = createSlice({
name: 'todos',
initialState: { items: [], filter: 'all' },
reducers: {
addTodo: {
reducer: (state, action) => { state.items.push(action.payload); }, // Immer
prepare: (text) => ({ payload: { id: crypto.randomUUID(), text, completed: false } })
},
toggleTodo: (state, action) => {
const t = state.items.find(t => t.id === action.payload);
if (t) t.completed = !t.completed;
},
setFilter: (state, action) => { state.filter = action.payload; }
}
});
export const { addTodo, toggleTodo, setFilter } = todosSlice.actions;
export const selectVisibleTodos = createSelector(
[s => s.todos.items, s => s.todos.filter],
(items, filter) =>
filter === 'completed' ? items.filter(t => t.completed) :
filter === 'active' ? items.filter(t => !t.completed) : items
);
export default todosSlice.reducer;
// store.js
import { configureStore } from '@reduxjs/toolkit';
import todos from './todosSlice';
export const store = configureStore({ reducer: { todos } });
// App.jsx
import { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { addTodo, toggleTodo, setFilter, selectVisibleTodos } from './todosSlice';
function AddTodo() {
const [text, setText] = useState('');
const dispatch = useDispatch();
return (
<form onSubmit={(e) => { e.preventDefault(); if (text.trim()) { dispatch(addTodo(text)); setText(''); } }}>
<input value={text} onChange={(e) => setText(e.target.value)} placeholder="Add a todo" />
<button>Add</button>
</form>
);
}
function TodoList() {
const visible = useSelector(selectVisibleTodos);
const dispatch = useDispatch();
return (
<ul>
{visible.map(t => (
<li key={t.id} onClick={() => dispatch(toggleTodo(t.id))}
style={{ textDecoration: t.completed ? 'line-through' : 'none' }}>
{t.text}
</li>
))}
</ul>
);
}
export default function App() {
const dispatch = useDispatch();
return (
<div>
<AddTodo />
<div>
{['all', 'active', 'completed'].map(f =>
<button key={f} onClick={() => dispatch(setFilter(f))}>{f}</button>
)}
</div>
<TodoList />
</div>
);
}
That's a complete, idiomatic modern Redux feature: one slice, one store, a Provider, and components that read via useSelector and write via useDispatch.
🎯 Quick Quiz
Question 1: What is the job of the <Provider> component?
Question 2: A component calls useSelector(state => ({ a: state.a, b: state.b })) and re-renders on every store update. Why?
Question 3: For new React function components, which approach does the Redux team recommend?
Summary & Quiz
🎉 Key Takeaways
- react-redux bridges Redux and React; install it with Redux Toolkit.
<Provider store={store}>wraps the app once and shares the store via context.useSelectorreads and subscribes to a slice;useDispatchsends actions.- Avoid returning fresh objects from a selector — select narrowly, memoize with
createSelector, or passshallowEqual. connect()is the legacy HOC; hooks are the modern default.- Keep UI presentational and test with a real store inside
<Provider>using Testing Library.
📚 Further Reading
- React-Redux Hooks API
- React-Redux — Provider
- Redux — Writing Tests
- Redux Essentials — App Structure & Toolkit
🚀 What's Next?
You've now written Redux the "classic" way and the modern way side by side. Next we go all-in on Redux Toolkit — the officially recommended toolset — and see how createSlice, configureStore, and friends collapse all this boilerplate into a fraction of the code.
🎉 You connected the whole stack!
Store, actions, reducers, and now a live React UI reading and dispatching. That's real state management — nicely done.