How to Use Redux in React: A Real-World Guide

As React applications grow in size and complexity, managing state shared across many components and deep component trees can become challenging. This is where a state management library like Redux comes to the rescue.

In this guide, we‘ll dive into what Redux is, why you might use it with React, and walk through real-world examples of implementing Redux in a React app step-by-step. By the end, you‘ll have a solid grasp of core Redux concepts and be able to apply them in your own projects. Let‘s get started!

What is Redux?

At its core, Redux is a predictable state container for JavaScript apps. It helps you manage global application state in a single store, making state changes predictable and traceable.

The key principles of Redux are:

  1. Single source of truth: The global state of your app is stored as a single object tree within a single Redux store.
  2. State is read-only: The only way to change the state is to dispatch an action, an object describing what happened.
  3. Changes are made with pure functions: Reducers are pure functions that take the previous state and an action, and return the next state. They specify how the state changes in response to actions.

Redux is often used with React, but it‘s an independent library that can be used with any UI layer or framework. The official React bindings are a separate package called React Redux.

Setting up Redux in a React Project

Before diving into code examples, let‘s quickly cover how to add Redux to a React project.

If you‘re starting a new project, you can use Create React App with a Redux template:

npx create-react-app my-app --template redux

For an existing project, you‘ll need to install the redux and react-redux packages:

npm install redux react-redux

With the setup out of the way, let‘s look at our first Redux and React example.

Example 1: Counter App

We‘ll start with a classic counter example – an app that lets you increment, decrement, or reset a counter. While simple, it covers the core concepts of actions, reducers, store, and connecting Redux to React components.

Step 1: Create the Redux Store

First, we‘ll create the Redux store that will hold the global state of our application. We need to define an initial state and a reducer function.

// store.js
import { createStore } from ‘redux‘;

// Define an initial state 
const initialState = {
  count: 0
};

// Create a "reducer" function that determines what the new state
// should be when something happens in the app
function counterReducer(state = initialState, action) {
  // Reducers usually look at the type of action that happened
  // to decide how to update the state
  switch (action.type) {
    case ‘counter/incremented‘:
      return { ...state, count: state.count + 1 };
    case ‘counter/decremented‘:
      return { ...state, count: state.count - 1 };
    case ‘counter/reset‘:
      return { ...state, count: 0 };
    default:
      // If the reducer doesn‘t care about this action type,
      // return the existing state unchanged
      return state;
  }
}

// Create a new Redux store with the `createStore` function,
// and use the `counterReducer` for the update logic
const store = createStore(counterReducer);

export default store;

Step 2: Define Action Creators

Next, we‘ll define functions that create actions, aptly named "action creators". Actions are plain JavaScript objects with a type property that describes what happened, and optional payload data.

// actions.js
export function increment() {
  return { type: ‘counter/incremented‘ };
}

export function decrement() {
  return { type: ‘counter/decremented‘ };
}

export function reset() {
  return { type: ‘counter/reset‘ };
}

Step 3: Connect React Component to Redux Store

Now we can create our React component and connect it to read data from the Redux store and dispatch actions to the store.

We‘ll use the useSelector hook to read data from the Redux store state and useDispatch to get access to the dispatch function to send actions to the store.

// Counter.js
import React from ‘react‘;
import { useSelector, useDispatch } from ‘react-redux‘;
import { increment, decrement, reset } from ‘./actions‘;

export function Counter() {
  // Get the current count value from Redux store state
  const count = useSelector(state => state.count);
  const dispatch = useDispatch();

  return (
    <div>

      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(decrement())}>-</button>
      <button onClick={() => dispatch(reset())}>Reset</button>
    </div>
  );
}

Step 4: Render the React Component in App

Finally, we need to wrap our entire application in a Redux <Provider> component to make the Redux store available throughout the component tree.

// App.js
import React from ‘react‘;
import { Provider } from ‘react-redux‘;
import store from ‘./store‘;
import { Counter } from ‘./Counter‘;

function App() {
  return (
    <Provider store={store}>
      <Counter />
    </Provider>
  );
}

export default App;

And there we have it! A working counter app using React and Redux. When the buttons are clicked, the corresponding actions are dispatched, the reducer updates the state based on those actions, and the React component re-renders with the new state value.

Example 2: Shopping Cart

For a more realistic example, let‘s build a basic e-commerce shopping cart. Users can add products to the cart, remove items, update quantities, and complete the checkout process.

Step 1: Design the Store State

Our shopping cart state will need to keep track of the products added, the quantity of each, and some totals for number of items and price.

const initialState = {
  products: [],
  totalQuantity: 0,
  totalPrice: 0
};

Step 2: Define Actions and Reducer

We‘ll need actions for adding and removing products, as well as clearing the entire cart upon checkout.

// actions.js
export const addToCart = product => {
  return {
    type: ‘cart/addProduct‘,
    payload: product
  };
};

export const removeFromCart = productId => {
  return {
    type: ‘cart/removeProduct‘,
    payload: productId  
  };
};

export const clearCart = () => {
  return {
    type: ‘cart/clear‘
  };
};

Our reducer needs to handle each of these actions and update the state accordingly. Some key functionality:

  • addToCart should check if the product already exists and increment quantity if so, otherwise add a new product object
  • removeFromCart should remove the product entirely
  • clearCart resets the state to initial values
  • We need to recalculate the totals after any cart change
// reducer.js
const cartReducer = (state = initialState, action) => {
  switch (action.type) {
    case ‘cart/addProduct‘:
      const productToAdd = action.payload;
      const existingProduct = state.products.find(
        product => product.id === productToAdd.id
      );

      if (existingProduct) {
        // If product already in cart, increment quantity
        const updatedProducts = state.products.map(product =>
          product.id === productToAdd.id
            ? { ...product, quantity: product.quantity + 1 }
            : product
        );
        return {
          ...state,
          products: updatedProducts,
          totalQuantity: state.totalQuantity + 1,
          totalPrice: state.totalPrice + productToAdd.price
        };
      } else {
        // If product not in cart, add as new product object
        const newProduct = { ...productToAdd, quantity: 1 };
        return {
          ...state,
          products: [...state.products, newProduct],
          totalQuantity: state.totalQuantity + 1,
          totalPrice: state.totalPrice + productToAdd.price
        };
      }

    case ‘cart/removeProduct‘:
      const productIdToRemove = action.payload;
      const productToRemove = state.products.find(
        product => product.id === productIdToRemove
      );
      return {
        ...state,
        products: state.products.filter(
          product => product.id !== productIdToRemove
        ),
        totalQuantity: state.totalQuantity - productToRemove.quantity,
        totalPrice: state.totalPrice - (productToRemove.price * productToRemove.quantity) 
      };

    case ‘cart/clear‘:
      return initialState;

    default:
      return state;
  }
};

export default cartReducer;

Step 3: Create the React Components

Our main cart component will display the products, quantities, totals, and checkout button. We‘ll dispatch our actions based on user interaction.

We can also create separate components like <ProductItem> and <CartSummary> to keep things organized.

// Cart.js
import React from ‘react‘;
import { useSelector, useDispatch } from ‘react-redux‘;
import { clearCart } from ‘./actions‘;
import ProductItem from ‘./ProductItem‘;
import CartSummary from ‘./CartSummary‘;

const Cart = () => {
  const { products, totalQuantity, totalPrice } = useSelector(state => state);
  const dispatch = useDispatch();

  return (
    <div>
      <h2>Shopping Cart</h2>
      {products.length === 0 ? (
        <p>Your cart is empty</p>
      ) : (
        <div>
          {products.map(product => (
            <ProductItem key={product.id} product={product} />
          ))}
          <CartSummary totalQuantity={totalQuantity} totalPrice={totalPrice} />
          <button onClick={() => dispatch(clearCart())}>Checkout</button>
        </div>
      )}
    </div>
  );
};

export default Cart;

Step 4: Persist State to Local Storage

As a final touch, we can persist our cart state to the browser‘s local storage so it‘s not lost on refresh.

We can subscribe to the store and save the state any time it changes, and also load any saved state when the app first starts up.

// store.js
import { createStore } from ‘redux‘;
import cartReducer from ‘./reducer‘;

const initialState = localStorage.getItem(‘reduxState‘) 
  ? JSON.parse(localStorage.getItem(‘reduxState‘))
  : {
      products: [],
      totalQuantity: 0,
      totalPrice: 0
    };

const store = createStore(cartReducer, initialState);

store.subscribe(() => {
  localStorage.setItem(‘reduxState‘, JSON.stringify(store.getState()));
});

export default store;

Redux Toolkit

While we‘ve built our examples from scratch using the core Redux library, it‘s worth mentioning Redux Toolkit. It‘s the official, opinionated, batteries-included toolset for efficient Redux development.

Redux Toolkit includes utilities to simplify common use cases like store setup, defining reducers, immutable update logic, and more. It‘s beneficial to learn the fundamentals of Redux first, but Redux Toolkit can help streamline development as you build more complex applications.

Conclusion

Redux is a powerful tool for managing global state in a predictable fashion, especially when used with React. By understanding actions, reducers, store, and React-Redux bindings, you can build robust, scalable applications.

In this guide, we walked through the steps of setting up Redux, building a simple counter example, and a more realistic shopping cart application. We even saw how to persist our Redux state to local storage.

Using Redux does add some boilerplate code to your app, but the benefits of predictable state management, improved scalability, and great developer tools often make it worthwhile.

Remember, while Redux is commonly used with React, it‘s an independent library and can be used with any other view library too. The concepts you‘ve learned in this guide will apply across any Redux implementation.

Now that you have a solid foundation in Redux, go out there and build something amazing! Happy coding!

Similar Posts