Skip to main content

🔗 Connecting Redux to React

A Redux store on its own is just a plain JavaScript object with a dispatch method — it knows nothing about React. In this lesson you'll install the bridge that ties the two together: the official React-Redux library. You'll make the store available everywhere with <Provider>, then read and update it from components using the modern hooks.

Week 6 · Day 2 (Tuesday: React-Redux Integration) · Lecture 1

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain what the react-redux library adds on top of core Redux
  • Wrap an application in <Provider> so any component can reach the store
  • Read state with useSelector and send actions with useDispatch as the primary, recommended pattern
  • Read the older connect() / mapStateToProps pattern accurately when you meet it in existing code
  • Prevent wasteful re-renders with selector memoization and the right equality check
  • Structure connected components using the container/presentational split

Estimated Time: 70 minutes

Practice: Wire a live counter to a Redux store, then refactor a connect() component to hooks.

In This Lesson

Why React-Redux?

Core Redux is deliberately UI-agnostic. A store exposes three methods: getState() to read, dispatch(action) to update, and subscribe(listener) to be told when something changed. You could wire those into React by hand — subscribe in every component, call getState, and force a re-render — but you'd re-invent a lot of fiddly, bug-prone plumbing.

React-Redux is the official binding library maintained by the Redux team. It does that plumbing for you, correctly and efficiently: it hands the store down the component tree, subscribes each component only to the slice of state it actually uses, and batches updates so React re-renders as little as possible.

🌉 The bridge analogy

Picture two islands. On one live your React components (the UI); on the other lives your Redux store (the data). React-Redux is the bridge between them. <Provider> is the bridge's foundation — it plants the store where every component can reach it. useSelector and useDispatch are the two lanes of traffic: one carries data from the store into your component, the other carries actions back to the store.

Everything in this lesson comes down to two moves: (1) make the store available once at the top, and (2) read and write it from any component below.

📦 Installing it

npm install react-redux
// You also need the core store libraries:
npm install @reduxjs/toolkit   // the modern, recommended way to build the store
// or the classic package if a codebase still uses it:
npm install redux

We'll write plain redux store setup here so the mechanics are visible, but Redux Toolkit (configureStore) is what you'd reach for in a new project — it wires DevTools and middleware for you.

Step 1: The Provider

Before any component can read the store, the store has to be reachable. React-Redux uses React Context under the hood, and <Provider> is the component that puts your store into that context. Wrap it around the very top of your app — once — and every component inside gains access.

The Redux store flows into Provider at the app root and down to every nested component Redux store <Provider store> <App /> <Header /> useSelector <Counter /> useDispatch <TodoList /> useSelector
One <Provider> at the root makes the store reachable by every component below — no prop-drilling required.

In a modern React 18+ app you mount with createRoot:

// main.jsx — the single entry point of the app
import React from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './reducers';
import App from './App';

// Build the store once, at module scope — NOT inside a component.
const store = configureStore({ reducer: rootReducer });

createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

If a project still uses classic Redux, the only line that changes is how the store is built:

import { createStore } from 'redux';   // legacy — still works, but Toolkit is preferred
const store = createStore(rootReducer);
// ...everything else (the Provider wrapping) is identical.

⚠️ Create the store outside the component

Notice store is created at module scope, not inside App. If you call configureStore during render, React makes a brand-new store on every render, throwing away all your state. Build it once and pass the stable reference to <Provider>.

Step 2: Reading & Dispatching with Hooks

With the store in context, components talk to it through two hooks. This is the primary, recommended API for all new code — it's less boilerplate than connect() and reads top-to-bottom like ordinary React.

graph LR A["Component"] --> B["useSelector(fn)"] B --> C["reads a slice of state"] A --> D["useDispatch()"] D --> E["dispatch(action)"] E --> F["reducer runs"] F --> G["new state"] G --> B

useSelector — read state

useSelector takes a selector function: it receives the whole state and returns just the piece this component cares about. React-Redux subscribes the component to the store and re-renders it whenever that returned value changes.

import { useSelector } from 'react-redux';

function CountDisplay() {
  // Read one slice. This component re-renders only when
  // state.counter.value changes — not on every store update.
  const count = useSelector((state) => state.counter.value);

  return <h2>Count: {count}</h2>;
}

useDispatch — send actions

useDispatch returns the store's dispatch function. Call it with an action object (or an action creator that returns one) to trigger a state change.

import { useDispatch } from 'react-redux';
import { increment, decrement } from './counterSlice';

function CounterControls() {
  const dispatch = useDispatch();

  return (
    <div>
      <button onClick={() => dispatch(decrement())}>−</button>
      <button onClick={() => dispatch(increment())}>+</button>
    </div>
  );
}

Both together: a self-contained counter

A single component can read and write. This is the whole React-Redux loop in one place:

import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './counterSlice';

function Counter() {
  const count = useSelector((state) => state.counter.value); // read
  const dispatch = useDispatch();                            // get dispatch

  return (
    <div>
      <h2>Count: {count}</h2>
      <button onClick={() => dispatch(decrement())}>−</button>
      <button onClick={() => dispatch(increment())}>+</button>
    </div>
  );
}

✅ Why this is the default

No wrapper functions, no higher-order component, no mapStateToProps to keep in sync. The data flow is visible right where it's used, and the component is trivially testable. Any tutorial or codebase written after 2019 will use these hooks first.

The Older connect() Pattern

Before hooks arrived (React-Redux v7.1, 2019), the only way to bind a component was the connect() higher-order component. You'll still meet it constantly in existing projects, so you need to read it fluently — even though you'll write hooks.

connect() takes two arguments and returns a function that wraps your component, injecting store data and dispatchers as props:

import { connect } from 'react-redux';
import { increment, decrement } from './actions';

// A plain "presentational" component — it just receives props,
// and has no idea Redux exists.
function Counter({ count, increment, decrement }) {
  return (
    <div>
      <h2>Count: {count}</h2>
      <button onClick={decrement}>−</button>
      <button onClick={increment}>+</button>
    </div>
  );
}

// mapStateToProps: pick data out of the store → becomes props.
const mapStateToProps = (state) => ({
  count: state.counter.value,
});

// mapDispatchToProps: as an object, each key is wrapped in dispatch
// for you, so `increment` prop === () => dispatch(increment()).
const mapDispatchToProps = { increment, decrement };

// connect(...) returns an HOC; call it with the component to wrap.
export default connect(mapStateToProps, mapDispatchToProps)(Counter);

How it maps to hooks

connect() conceptHooks equivalent
mapStateToPropsOne or more useSelector calls
mapDispatchToPropsuseDispatch + call the action creator
The wrapping HOCNone — the component uses the store directly
Injected as propsRead as local variables inside the component

mapDispatchToProps can also be written as a function when you need to shape the dispatch calls yourself:

const mapDispatchToProps = (dispatch) => ({
  increment:   () => dispatch(increment()),
  decrement:   () => dispatch(decrement()),
  incrementBy: (amount) => dispatch({ type: 'counter/incrementBy', payload: amount }),
});

💡 When you might still write connect()

The Redux team keeps connect() fully supported — it isn't deprecated. It shines when you want a strict separation between "dumb" presentational components and their data source, or when working in a class-component codebase where hooks aren't available. For everything else, prefer hooks.

Selectors, Memoization & Equality

React-Redux re-renders a component when the value its selector returns changes. "Changes" is decided by a comparison — and by default that comparison is strict reference equality (===). Understanding this is the key to fast Redux apps.

The new-object trap

// ❌ Returns a BRAND-NEW object literal every render.
// Even if user and posts are unchanged, the object is a new
// reference each time, so === is false → re-render every store update.
const { user, posts } = useSelector((state) => ({
  user: state.user,
  posts: state.posts,
}));

Fix 1: select the smallest pieces separately

// ✅ Each returns a primitive or stable reference.
// The component re-renders only when that specific slice changes.
const user  = useSelector((state) => state.user);
const posts = useSelector((state) => state.posts);

Fix 2: tell useSelector to compare shallowly

import { shallowEqual } from 'react-redux';

// ✅ Pass a second arg: compare the returned object field-by-field
// instead of by reference. No re-render unless a field actually changed.
const data = useSelector(
  (state) => ({ user: state.user, posts: state.posts }),
  shallowEqual
);

Fix 3: memoize derived data with Reselect

When a selector computes something (filters, sorts, totals), recomputing on every render is wasteful and produces a new reference each time. createSelector from Reselect (bundled into Redux Toolkit) caches the result and only recomputes when its inputs change:

import { createSelector } from '@reduxjs/toolkit'; // re-exports reselect

const selectTodos  = (state) => state.todos;
const selectFilter = (state) => state.filter;

// Recomputes ONLY when todos or filter changes; otherwise returns
// the exact same array reference → no needless re-renders.
const selectVisibleTodos = createSelector(
  [selectTodos, selectFilter],
  (todos, filter) => {
    switch (filter) {
      case 'completed': return todos.filter((t) => t.completed);
      case 'active':    return todos.filter((t) => !t.completed);
      default:          return todos;
    }
  }
);

function TodoList() {
  const visible = useSelector(selectVisibleTodos); // memoized
  return <ul>{visible.map((t) => <li key={t.id}>{t.text}</li>)}</ul>;
}

Rule of thumb

Selecting a raw slice?      → plain useSelector is fine
Selecting several values?   → separate calls OR shallowEqual
Computing/deriving a value? → memoize with createSelector

Container / Presentational Pattern

A durable way to organize connected components is to split them in two: a container that talks to Redux, and a presentational component that only knows about its props. The presentational piece stays pure, reusable, and easy to test in isolation.

// Presentational — no Redux, just props. Reusable anywhere.
function TodoList({ todos, onToggle }) {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id} onClick={() => onToggle(todo.id)}>
          {todo.completed ? '✓ ' : '○ '}{todo.text}
        </li>
      ))}
    </ul>
  );
}

// Container — the only part that knows about the store.
function TodoListContainer() {
  const todos = useSelector((state) => state.todos);
  const dispatch = useDispatch();
  return <TodoList todos={todos} onToggle={(id) => dispatch(toggleTodo(id))} />;
}

Modern codebases often collapse the two — a component can perfectly well call the hooks itself — but the separation is invaluable when the presentational piece is shared, or when you want to test rendering without a store.

🪝 Bonus: extract a custom hook

You can also bottle a chunk of store logic into a reusable hook, which reads even cleaner than a container:

function useTodos() {
  const todos = useSelector((state) => state.todos);
  const dispatch = useDispatch();
  const toggle = (id) => dispatch(toggleTodo(id));
  return { todos, toggle };
}

function TodoList() {
  const { todos, toggle } = useTodos(); // clean, testable, reusable
  // ...render
}

Practice & Quiz

🏋️ Exercise 1: Wire up a counter

Goal: Given a store whose state is { counter: { value: 0 } } and action creators increment() and decrement(), complete the Counter component using the hooks API so the buttons update the displayed count.

import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './counterSlice';

function Counter() {
  // TODO: read state.counter.value
  const count = /* ... */;
  // TODO: get dispatch
  const dispatch = /* ... */;

  return (
    <div>
      <h2>Count: {count}</h2>
      <button onClick={/* dispatch decrement */}>−</button>
      <button onClick={/* dispatch increment */}>+</button>
    </div>
  );
}
💡 Hint

useSelector takes a function that receives state and returns the piece you want. useDispatch() returns the dispatch function — call it with an action creator, e.g. dispatch(increment()).

✅ Solution
function Counter() {
  const count = useSelector((state) => state.counter.value);
  const dispatch = useDispatch();

  return (
    <div>
      <h2>Count: {count}</h2>
      <button onClick={() => dispatch(decrement())}>−</button>
      <button onClick={() => dispatch(increment())}>+</button>
    </div>
  );
}

🏋️ Exercise 2: Refactor connect() to hooks

Goal: Convert this legacy component to the modern hooks API. It should behave identically.

function UserBadge({ name, notify }) {
  return <button onClick={notify}>Hello, {name}</button>;
}
const mapStateToProps = (state) => ({ name: state.user.name });
const mapDispatchToProps = { notify: sendPing };
export default connect(mapStateToProps, mapDispatchToProps)(UserBadge);
✅ Solution
import { useSelector, useDispatch } from 'react-redux';
import { sendPing } from './actions';

function UserBadge() {
  const name = useSelector((state) => state.user.name);
  const dispatch = useDispatch();
  return <button onClick={() => dispatch(sendPing())}>Hello, {name}</button>;
}
export default UserBadge;

mapStateToProps becomes a useSelector; the mapDispatchToProps entry becomes dispatch(sendPing()); the HOC wrapper disappears entirely.

🎯 Quick Quiz

Question 1: Which component makes the Redux store available to the rest of the app?

Question 2: Why can returning a new object literal from useSelector cause extra re-renders?

Question 3: In the modern hooks API, what replaces mapDispatchToProps?

Best Practices & Pitfalls

✅ Do

  • Reach for useSelector / useDispatch first in all new code
  • Create the store once at module scope and pass it to a single <Provider> at the root
  • Select the smallest slice a component needs — narrow selectors mean fewer re-renders
  • Memoize derived/computed data with createSelector
  • Reach for shallowEqual when a selector genuinely must return an object of several fields

❌ Don't

  • Don't build the store inside a component's render — it resets state every render
  • Don't return a fresh object/array literal from useSelector without shallowEqual or memoization
  • Don't nest multiple <Provider>s for the same store
  • Don't reach for connect() in new code just out of habit — hooks are the recommended path
  • Don't put non-serializable values (functions, class instances) into the store

⚠️ dispatch is stable — leave it out of nothing important

The function returned by useDispatch keeps the same identity for the life of the store. It's safe to include in a useEffect or useCallback dependency array (React's lint rules will ask you to), and doing so won't cause the effect to re-run.

Summary

🎉 Key Takeaways

  • React-Redux is the official bridge; it handles subscriptions and re-renders so you don't have to
  • Wrap the app once in <Provider store={store}> to make the store reachable everywhere
  • Read state with useSelector and dispatch with useDispatch — the modern default
  • The older connect() + mapStateToProps pattern still works and is common in existing code — read it, but write hooks
  • Control re-renders with narrow selectors, shallowEqual, and memoized createSelector

📚 Additional Resources

🚀 What's Next?

You've met the two hooks in passing — next we go deep. The following lesson, useSelector and useDispatch hooks, unpacks selector patterns, factory selectors, async dispatch with thunks, and the subtle equality rules that keep a Redux app fast.

🎉 The bridge is built!

Your React components and your Redux store can finally talk. Everything from here is about doing it cleanly and quickly.