Skip to main content

🔌 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 useSelector and send actions with useDispatch
  • 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.

flowchart TB App[React App] App --> Provider["<Provider store={store}>"] Provider --> Comp[React Components] Comp -->|"useSelector reads"| State[Redux State] Comp -->|"useDispatch sends"| Dispatch[dispatch action] Dispatch --> Store[Redux Store] Store --> State

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's dispatch function.

📖 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 codeNeeded for class components
Less boilerplate, reads top-to-bottomExplicit map functions, more ceremony
Data dependencies live inline in the componentData dependencies declared separately
Recommended going forwardFully 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

  1. Normalize state to avoid deep nesting.
  2. Select narrowly — the smallest slice each component needs.
  3. Memoize derived data with createSelector.
  4. Avoid new objects/arrays inside useSelector (or use shallowEqual).
  5. React.memo pure child components.
  6. 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.

flowchart TD Store[Redux Store] --> Hook["useSelector / useDispatch"] Hook --> Pres["Presentational Components
(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:

  1. Create a todosSlice with createSlice: state { items: [], filter: 'all' } and reducers addTodo, toggleTodo, setFilter.
  2. Build the store with configureStore and wrap <App /> in <Provider>.
  3. Write an AddTodo form (useDispatch) and a TodoList (useSelector with 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.
  • useSelector reads and subscribes to a slice; useDispatch sends actions.
  • Avoid returning fresh objects from a selector — select narrowly, memoize with createSelector, or pass shallowEqual.
  • 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

🚀 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.