Connecting Redux to React

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

graph TD A[Redux Store] --> B[Provider Component] B --> C[App Component] C --> D[Component Tree] D --> E[Connected Components] E --> F[useSelector/useDispatch] style B fill:#f96 style E fill:#9cf style F fill:#9f9

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

graph TD A[Performance Issues] --> B[Unnecessary Re-renders] A --> C[Large State Objects] A --> D[Complex Selectors] B --> E[Use Memoization] C --> F[Normalize State] D --> G[Reselect Library] style A fill:#f96 style E fill:#9cf style F fill:#9f9 style G fill:#ff9

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
};
                

Additional Resources