Mastering state management with Redux

Mastering state management with Redux

Learn how to use Redux for global state management and interact with third-party APIs to create an awesome movie application

Introduction

It is quite common to come across scenarios in your application where you need somehow to access the state, which is shared among various components scattered across the component tree. In such cases, relying solely on props drilling or context may not always be the most effective approach.

This is where Redux comes in handy! It serves as a state container for your application, providing a centralized location to store and manage the state. This global state management approach allows for easy access and efficient management of the application's data.

Imagine a scenario where you want to store a collection of favorites, be it products, music, movies, etc. In today's tutorial, I will show how you can accomplish this using Redux.

Together, we’ll walk through the entire process, starting from configuring the Redux store to handling the logic of adding and removing items from the storage.

As we progress, we will also explore the process of interacting with third-party API to retrieve information as well as leverage React Bootstrap, which is a powerful frontend toolkit with a huge collection of UI elements that allows us to quickly and elegantly style our application.

Upon completion of this tutorial, you will gain the necessary knowledge and skills to build an application similar to the one shown in the demo below:

In our application, our primary focus will be on storing and managing the list of the user’s favorite movies. The user will be presented with a list of movies, and they will have the ability to add or remove movies from their favorites list.

Redux Basics

Before jumping into coding, it is important to understand the fundamentals of Redux and familiarize ourselves with key terms associated with working in the Redux ecosystem.

Here is a schema showing the underlying mechanism of Redux. Now, let's analyze and break down the key components and their roles in this process.

I'll give you a quick rundown, but if you want to dive deeper, I recommend checking out the Redux glossary.

  1. By interacting with our user interface (UI), the user is able to trigger the dispatching of an action. One common example is when the user presses a button, which in turn initiates an event that requires a change in the global state.

  2. An action is an object that represents the user's intention to modify the current state. It serves as the sole method to update the data stored in the Redux store, as the state within Redux is typically treated as read-only.

  3. The action is dispatched to the reducer.

  4. The reducer is simply a function that takes the current state and the dispatched action as parameters. It evaluates the new state based on these inputs and returns the updated state. Since the state in Redux is immutable, the reducer first makes a copy of the entire state before making any changes.

  5. The store serves as the central hub where the application state is stored. It is highly recommended to have just one store in your application.

The key concept is that since we’ve got the unique store, maintained as the single source of truth, we can retrieve the data from it, i.e. get the state, anywhere inside our application, not depending on the position in the component tree.

I understand that it may feel overwhelming with all this information, but as we start the development process, everything will start to make sense. The key concept to understand is that with the singular store serving as the single source of truth, we can easily access the data from any component within our application, regardless of its position in the component tree.

Getting Started

Another important point to mention is that in this tutorial we’ll use Redux Toolkit, which is the officially recommended approach for writing Redux logic. Basically, Redux Toolkit is a set of tools that aim to simplify Redux development.

Alright, let's get started! The first step is to initialize and set up our application.

To initiate the project setup, we will use Vite. To create the application, simply run the following command in your terminal:

npm create vite@latest

Afterward, we’ll configure the application as a React project with JavaScript:

✔ Project name: … favorite-movies
✔ Select a framework: › React
✔ Select a variant: › JavaScript

Next, let's install the React Bootstrap by running this in the terminal:

npm install react-bootstrap bootstrap

After that, we need to import the CSS file that contains the default styling provided by Bootstrap. This step is crucial for the library to function properly.

// main.jsx

import "bootstrap/dist/css/bootstrap.min.css"
import React from "react"
import ReactDOM from "react-dom/client"
import App from "./App.jsx"

ReactDOM.createRoot(document.getElementById("root")).render(
    <React.StrictMode>
        <App />
    </React.StrictMode>
)

Great! With the initial setup complete, we can now proceed with developing the core logic of our application.

To get movie information, we will send an API request to the TMDb API. This API will provide us with a list of objects, each representing a specific movie.

Since we don't know the exact details about the movie information yet, we'll set up the Redux store and its important components later. However, if you want to learn primarily about Redux, feel free to skip ahead to the dedicated section that specifically covers Redux.

At this moment, our main focus will be on creating the pages for our application. We will have two pages: Home and Favorites.

  • On the Home page, we’ll fetch movie data, present it to the user, and allow the user to add any desired movie to their favorites collection.

  • On the Favorites page, we’ll simply display all the movies that have been marked as favorites by the user.

Fetching the movie data

Getting the API key

To make your own API request, you will first need to create an account with the TMDb API

After successfully creating your account, you can obtain a unique API key from your dashboard:

We are now fully prepared to learn the process of fetching movie information. The TMDb API offers a wide range of options for discovering details about movies. In our specific case, we’ll just focus on fetching a list of the 20 most popular movies.

For a practical example, you can refer to the documentation provided by TMDb

It's worth mentioning that the Redux Toolkit provides its own powerful data-fetching solution known as Redux Toolkit Query. However, as our main focus is on managing data in the Redux store rather than the data fetching itself, we will use a simpler approach with the React useEffect hook and the built-in fetch API. While this approach may not be ideal, it allows us to keep the data-fetching process straightforward and aligned with our primary objectives.

Home page

Now we can kick off by creating the Home page. To do this, we’ll create the pages folder and include a file named Home.jsx within it.

As a way to verify that everything is functioning correctly, let's try to fetch the list of popular movies and display them in the console.

// pages/Home.jsx

function Home() {
   useEffect(() => {
    // URL of the request:
    const url = "https://api.themoviedb.org/3/movie/popular"
    const options = {
        method: "GET",
        headers: {
            "Content-Type": "application/json",
            // Passing API key provided by TMDb API:
            "Authorization": "Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI3NDFmYmRiODk0ZDlmZDM1MTgyODc2NGMxOTY2OTJmYiIsInN1YiI6IjY0OWFmYTNlMjk3NWNhMDBjODgyZjg4MCIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.fTMguHXL_Pk9t4g7d-7HjtX_vK7ZwccR9AIPntppH1g"
        }
    }

    // Fetching the data and printing in the console
    fetch(url, options)
       .then(res => res.json())
       .then(data => console.log(data))
    }, [])

    return null;
}

Remember to include the Home component within the App component:

function App() {
    return <Home />
}

Essentially, we're sending a GET request to the API endpoint specified in the URL, and within the request header, we're passing the unique authorization key associated with our account. At this stage, we're just trying to display the fetched data in the console. The result will be as follows:

So, we can notice that it is an array with 20 objects, where each object represents a unique movie. Now, let's take a closer look at what each of these objects actually contains:

Now that we have a clear understanding of the data we're dealing with, we can start displaying the information about each movie. Let's create a separate component that represents a card to showcase the movie details passed to it as props. To do this, we’ll create the MovieCard component inside a new folder named components:

// components/MovieCard.jsx

import Card from "react-bootstrap/Card"
import ListGroup from "react-bootstrap/ListGroup"

const MovieCard = (props) => {
    return (
        <Card style={{ width: "18rem" }} className="flex-shrink-0">
            <Card.Img 
                variant="top" 
                src={"https://image.tmdb.org/t/p/original" + props.posterPath}
            />
            <Card.Body>
                <Card.Title>{props.title}</Card.Title>
                <Card.Text>
                    {props.overview}
                </Card.Text>
            </Card.Body>
            <ListGroup className="list-group-flush">
                <ListGroup.Item>Rating: {props.voteAverage}</ListGroup.Item>
                <ListGroup.Item>Released: {props.releaseDate}</ListGroup.Item>
            </ListGroup>
        </Card>
    )
}

In the code provided above, we are using the Card component and its related components from React Bootstrap. The card serves as a flexible and customizable container for content display. For further information, you can read more about it here.

You might have noticed that the links to the images are not complete. Fortunately, TMDb API provides guidance on how to construct the full link. In short, you can simply append "image.tmdb.org/t/p/original" to the beginning of the image path to form the complete link.

Now we want to add a button to each movie card in the form of an icon. To accomplish this, we’ll use react-bootstrap-icons library that provides us with access to Bootstrap icons within our React application.

To install the necessary packages, we can use the following command:

npm i react-bootstrap-icons

Next, we will import two icons—one representing the icon for a favorite movie and the other for a non-favorite movie. Here is the updated code snippet:

// components/MovieCard.jsx

import Card from "react-bootstrap/Card"
import ListGroup from "react-bootstrap/ListGroup"
import Button from "react-bootstrap/Button"
import { Heart, HeartFill } from "react-bootstrap-icons"

const MovieCard = (props) => {
    return (
        <Card style={{ width: "18rem", position: "relative" }} className="flex-shrink-0 m-2">
            <Card.Img 
                variant="top" 
                src={"https://image.tmdb.org/t/p/original" + props.posterPath}
            />
            <Card.Body className="flex-grow-1">
                <Card.Title>{props.title}</Card.Title>
                <Card.Text>
                    {props.overview}
                </Card.Text>
            </Card.Body>
            <ListGroup className="list-group-flush">
                <ListGroup.Item>Rating: {props.voteAverage}</ListGroup.Item>
                <ListGroup.Item>Released: {props.releaseDate}</ListGroup.Item>
            </ListGroup>
            <Button 
                variant="danger"
                style={{ position: "absolute", right: "5px", top: "5px" }}
            >
                <Heart />
            </Button>
        </Card>
    )
}

Now, on the Home page, we need to display the list of our movie cards, complete with all the movie details. To do so, we'll create a new state within our Home component to store the fetched data from the API. Then, we'll use map() function to iterate over this state and pass the movie information to the corresponding MovieCard component. Below, you can find the code snippet for this:

// pages/Home.jsx

import { useEffect, useState } from "react"
import MovieCard from "../components/MovieCard"

function Home() {
    // Storing the movie collection within the component's state
    const [movies, setMovies] = useState([])

    useEffect(() => {
        const url = "https://api.themoviedb.org/3/movie/popular"

        const options = {
            method: "GET",
            headers: {
                "Content-Type": "application/json",
                "Authorization": "Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI3NDFmYmRiODk0ZDlmZDM1MTgyODc2NGMxOTY2OTJmYiIsInN1YiI6IjY0OWFmYTNlMjk3NWNhMDBjODgyZjg4MCIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.fTMguHXL_Pk9t4g7d-7HjtX_vK7ZwccR9AIPntppH1g"
            }
        }

        // Storing the fetched data inside the state
        fetch(url, options)
            .then(res => res.json())
            .then(data => setMovies(data.results))
    }, [])

    if (!movies) {
        return null;
    }

    return (
        <div className="container pt-5">
            <div className="d-flex justify-content-between align-items-center">
                <h2 className="fw-bold">List of Movies</h2>
            </div>
            <div className="d-flex flex-wrap">
                {movies.map((movie) => (
                    <MovieCard 
                        key={movie.id}
                        id={movie.id}
                        title={movie.title}
                        posterPath={movie.poster_path}
                        overview={movie.overview}
                        vote_Average={movie.vote_average}
                        releaseDate={movie.release_date}
                    />
                ))}
            </div>
        </div>
    )
}

Keep in mind that the API initially provides the fields in snake case format, but I'm converting them into camel case when passing them as props to the MovieCard component.

Great job! At this point, we have some visually appealing movie cards. The result should look similar to this:

Just to make sure we're on the same page, here is my current project structure:

We've got a lot more exciting stuff ahead, so let's keep up the good work! 😊

Configuring Redux

Alright, now we're ready to start working with Redux 🚀

To get started, let's install Redux and Redux Toolkit by running the following command in the terminal:

npm install @reduxjs/toolkit redux

Afterward, we'll create and configure the Redux store, which is a crucial part of any Redux application.

Within the src directory, we're going to set up a new folder named app containing a file named store.js. The code to configure the store will appear as follows:

// app/store.js

import { configureStore } from "@reduxjs/toolkit"

// Creating empty store
export const store = configureStore({
    reducer: {},
})

Afterward, to make the Redux store accessible to all components, we should wrap our App component with Provider like this:

import { Provider } from "react-redux"
import { store } from "./app/store.js"

ReactDOM.createRoot(document.getElementById("root")).render(
    <React.StrictMode>
        <Provider store={store}>
            <App />
        </Provider>
    </React.StrictMode>,
)

We're ready to create a slice. Basically, a slice is a set of Redux reducer logic and actions dedicated to handling a specific feature in our app. In our case, this slice will handle how we store and manage our favorite movies.

Let's create a new folder named features/favorites with a file favoritesSlice.js inside of it. To develop the slice, we'll provide a string name, which identifies the slice, an initial state value, and reducer functions that define how the state gets updated.

// features/favorites/favoritesSlice.js

import { createSlice } from "@reduxjs/toolkit";

export const favoritesSlice = createSlice({
    name: "favorites",
    initialState: {
        movies: []
    },
    reducers: {
    }
})

Next up, we'll define the reducers. Specifically, we'll have two reducers for our case: one for adding to the list of favorites and another for removing from the favorites. Let's start with the reducer for adding movies:

reducers: {
        addToFavorites: (state, action) => {
            state.movies.push(action.payload)
        }
}

Keep in mind that the reducer is a pure function that accepts both the state and dispatched action as inputs. In our scenario, the favorites are represented by an array containing movie objects. Therefore, we can just directly add a fresh object, like the most recent favorite movie, to the end of this array.

Take note that to obtain this list, we're using the state parameter. The data dispatched alongside the action can be accessed through the payload inside of that action object.

Next, let's work on the process of eliminating a movie from the list of favorites:

reducers: {
    removeFromFavorites: (state, action) => {
        state.movies = state.movies.filter((movie) => movie.id !== action.payload.id)
    }
}

In this code snippet, we're updating our movie list using the filter() method. This method generates a new array without the element we intend to remove. Since each movie object has a unique ID, we can compare the IDs of the movies in the array with the ID of the movie we want to delete.

The final task within favoritesSlice is to export the action creators that are generated for each reducer function:

// features/favorites/favoritesSlice.js

import { createSlice } from "@reduxjs/toolkit";

export const favoritesSlice = createSlice({
    name: "favorites",
    initialState: {
        movies: []
    },
    reducers: {
        addToFavorites: (state, action) =>  {
            state.movie.push(action.payload)
        },
        removeFromFavorites: (state, action) => {
            state.movies = state.movies.filter((movie) => movie.id !== action.payload.id)
        }
    }
})


export const { addToFavorites, removeFromFavorites } = favoritesSlice.actions
export default favoritesSlice.reducer

Now, let's include the slice reducers for the newly created slice in the store:

import { configureStore } from "@reduxjs/toolkit"
import favoritesReducer from "../features/favorites/favoritesSlice"

export const store = configureStore({
    reducer: {
        favorites: favoritesReducer
    }
})

Integrating User Interface with Redux

Adding or Removing Movies from Favorites

Since we've got the reducers ready, we can integrate them into our MovieCard component. Let's not forget about the button, which previously had no functionality. It's time for the button to shine 🌟

Let's break down the issue and tackle it step by step:

  1. We will create a new state within the MovieCard component to identify whether this specific movie exists in the favorites list or not.
    Then, we'll assign the onClick event handler to the button, which causes toggling of this state.
// components/MovieCard.jsx

const MovieCard = (props) => {
    // Creating the state that indicates whether
    // the movie is inside the favorites list
    const [isFavorite, setIsFavorite] = useState(false)

    // Handling button click
    const handleFavoriteChange = () => {
        setIsFavorite((prevFavorite) => !prevFavorite)
    };

    return (
        <Card style={{ width: "18rem" }} className="flex-shrink-0 m-2">
            <Card.Img 
                variant="top" 
                src={"https://image.tmdb.org/t/p/original" + props.posterPath}
            />
            <Card.Body className="flex-grow-1">
                <Card.Title>{props.title}</Card.Title>
                <Card.Text>
                    {props.overview}
                </Card.Text>
            </Card.Body>
            <ListGroup className="list-group-flush">
                <ListGroup.Item>Rating: {props.voteAverage}</ListGroup.Item>
                <ListGroup.Item>Released: {props.releaseDate}</ListGroup.Item>
            </ListGroup>
            <Button 
                variant="danger"
                onClick={handleFavoriteChange}
                style={{ position: "absolute", right: "5px", top: "5px" }}
            >
                {isFavorite ? <HeartFill /> : <Heart />}
            </Button>
        </Card>
    )
}

Currently, the button works in the following manner:

  1. We need to get our current list of favorites from the Redux store using useSelector() hook provided by Redux Toolkit:
import { useSelector } from "react-redux"

// Accessing the data inside Redux store
const favorites = useSelector(state => state.favorites.movies)

Using the retrieved data from the store, we will check whether this particular movie exists in the favorite list or not:

const [isFavorite, setIsFavorite] = useState(favorites.some((movie) => movie.id === props.id))

The some() method checks whether at least one element within the array has the same ID as the one we receive as a prop in the MovieCard component. This allows us to determine if the particular movie is included in the favorites. If the test passes, the state will be set to true; otherwise, it will be false.

  1. We will trigger actions using the useDispatch() hook, which is made available by Redux Toolkit:
import { useDispatch } from "react-redux"

// Creating object to dispatch the actions to the reducers
const dispatch = useDispatch()

We'll import the actions that are previously defined in the slice favoritesSlice, specifically those responsible for adding or deleting movies. Then, we are going to dispatch the appropriate action within the onClick event handler, passing the props from the MovieCard component as the action payload. The code snippet is presented as follows:

import { addToFavorites, removeFromFavorites } from "../features/favorites/favoritesSlice"

const handleFavoriteChange = () => {
    // If the movie is marked as a favorite, 
    // clicking the button will remove it from the favorites list. 
    // Otherwise, the button click will add it to the favorites.
    isFavorite ? dispatch(removeFromFavorites(props)) : dispatch(addToFavorites(props))

    setIsFavorite((prevFavorite) => !prevFavorite)
}

The full code looks the following way:

// components/MovieCard.jsx

import { useState } from "react"
import Card from "react-bootstrap/Card"
import ListGroup from "react-bootstrap/ListGroup"
import Button from "react-bootstrap/Button"
import { useDispatch, useSelector } from "react-redux"
import { addToFavorites, removeFromFavorites } from "../features/favorites/favoritesSlice"
import { Heart, HeartFill } from "react-bootstrap-icons"

const MovieCard = (props) => {
    const favorites = useSelector(state => state.favorites.movies)
    const [isFavorite, setIsFavorite] = useState(    favorites.some((movie) => movie.id === props.id))
    const dispatch = useDispatch()

    const handleFavoriteChange = () => {
        isFavorite ? dispatch(removeFromFavorites(props)) : dispatch(addToFavorites(props))

        setIsFavorite((prevFavorite) => !prevFavorite)
    }

    return (
        <Card style={{ width: "18rem", position: "relative" }} className="flex-shrink-0 m-2">
            <Card.Img 
                variant="top" 
                src={"https://image.tmdb.org/t/p/original" + props.posterPath}
            />
            <Card.Body className="flex-grow-1">
                <Card.Title>{props.title}</Card.Title>
                <Card.Text>
                    {props.overview}
                </Card.Text>
            </Card.Body>
            <ListGroup className="list-group-flush">
                <ListGroup.Item>Rating: {props.voteAverage}</ListGroup.Item>
                <ListGroup.Item>Released: {props.releaseDate}</ListGroup.Item>
            </ListGroup>
            <Button 
                variant="danger"
                onClick={handleFavoriteChange}
                style={{ position: "absolute", right: "5px", top: "5px" }}
            >
                {isFavorite ? <HeartFill /> : <Heart />}
            </Button>
        </Card>
    )
}

export default MovieCard

Congratulations on the progress so far! Only a few steps remain to complete the project. One of these steps involves creating a new page that shows all the favorite movies the user has added.

Favorites page

We will generate the file Favorites.jsx within the pages folder with the provided code snippet:

// pages/Favorites.jsx

import { useSelector } from "react-redux"
import MovieCard from "../components/MovieCard";

function Favorites() {
    const favorites = useSelector(state => state.favorites.movies)

    return (
        <div className="container pt-5">
            <div className="d-flex justify-content-between align-items-center">
                <h2 className="fw-bold">Favorites</h2>
            </div>
            <div className="d-flex flex-wrap">
                {favorites.map((movie) => (
                    <MovieCard 
                        key={movie.id}
                        id={movie.id}
                        title={movie.title}
                        posterPath={movie.posterPath}
                        overview={movie.overview}
                        voteAverage={movie.voteAverage}
                        releaseDate={movie.releaseDate}
                    />
                ))}
            </div>
        </div>    
    )
}

export default Favorites

The Favorites page is quite similar to the Home page. The main distinction lies in the fact that instead of fetching all available movies, we are now obtaining only our favorite ones using the useSelector() hook.

The final setup remaining is to implement routing using React Router. This tutorial assumes a basic familiarity with React Router. However, even if you are new to it, there's no need to worry as we will only be using the basic features for our project.

React Router

Let's install React Router by running the following command:

npm install react-router-dom

Now, within the main.jsx file, import BrowserRouter and wrap your App component with it. This will enable the routing functionality in your application:

// main.jsx

import React from "react"
import ReactDOM from "react-dom/client"
import App from "./App.jsx"
// Importing BrowserRouter
import { BrowserRouter } from "react-router-dom"

ReactDOM.createRoot(document.getElementById("root")).render(
    <React.StrictMode>
        <BrowserRouter>
            <App />
        </BrowserRouter>
    </React.StrictMode>
)

Next, we'll configure the routes inside the App component:

import Home from "./pages/Home"
import Favorites from "./pages/Favorites"
import { Route, Routes } from "react-router-dom"

function App() {
    return (
        <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/favorites" element={<Favorites />} />
        </Routes>
    )
}

To allow navigation from one page to another, we will use the Link component from React Router. This allows us to create clickable links that navigate between different pages of our application. Specifically, inside the Home page, we need to provide a link for the user to navigate to the Favorites page, and vice versa.

// pages/Home.jsx

import { Link } from "react-router-dom"

// REST OF THE CODE...

return (
  <div className="container pt-5">
      <div className="d-flex justify-content-between align-items-center">
          <h2 className="fw-bold">List of Movies</h2>
          {/* Link to the Favorites page */}
          <Link to="/favorites">To Favorites</Link>
      </div>
      <div className="d-flex flex-wrap justify-content-between">
          {movies.map((movie) => (
              <MovieCard 
                  key={movie.id}
                  id={movie.id}
                  title={movie.title}
                  posterPath={movie.poster_path}
                  overview={movie.overview}
                  voteAverage={movie.vote_average}
                  releaseDate={movie.release_date}
              />
          ))}
      </div>
  </div>
)
// pages/Favorites.jsx

import { Link } from "react-router-dom"

// REST OF THE CODE...

return (
  <div className="container pt-5">
      <div className="d-flex justify-content-between align-items-center">
          <h2 className="fw-bold">Favorites</h2>
          <Link to="/">To Home</Link>
      </div>
      <div className="d-flex flex-wrap">
          {favorites.map((movie) => (
              <MovieCard 
                  key={movie.id}
                  id={movie.id}
                  title={movie.title}
                  posterPath={movie.posterPath}
                  overview={movie.overview}
                  voteAverage={movie.voteAverage}
                  releaseDate={movie.releaseDate}
              />
          ))}
      </div>
  </div>    
)

And now, we can look at the functional version of our small application in action:

I'm sure you've probably noticed the problem or may have come across it before. When we refresh the page, the state goes back to its initial value, causing the store to lose all the favorite movies that were previously added. But don't worry, in this optional yet super useful section, I'll show you how to fix this issue.

Persisting the state in Redux

Redux Persist is an awesome library that allows us to preserve the Redux state even after a page refresh. To implement this functionality, the first step is to install the library by running the following command:

npm i redux-persist

Next, we'll make a few modifications to our store.js file:

  1. Combine the reducer using the combineReducers() method, which allows us to merge multiple reducing functions represented as objects into a single reducing function. In our case, there is only one reducer.
import { combineReducers } from "@reduxjs/toolkit"

const combinedReducer = combineReducers({ favorites: favoritesReducer })
  1. Create an object that represents the configuration for persistent storage, including a unique key and a storage adapter. In our application, we will store the data inside the local storage for persistence:
// Defaults to localStorage for web
import storage from "redux-persist/lib/storage"

// Config object
const persistConfig = {
    key: "favorites",
    storage
}
  1. Create the enhanced persisted reducer using persistReducer() by passing the newly defined configuration.
import { persistReducer } from "redux-persist"

const persistConfig = {
    key: "favorites",
    storage
}
const persistedReducer = persistReducer(persistConfig, combinedReducer)
  1. Update the old store with the new persisted reducer and create the persisted storage using persistStore()
import { persistStore } from "redux-persist"

export const store = configureStore({
    reducer: persistedReducer
})

export const persistor = persistStore(store)
  1. Wrap the App component with PersistGate component. This will defer the rendering of your app's UI until the persisted state has been retrieved and saved to Redux, ensuring that the application loads with the correct data:
import { PersistGate } from "redux-persist/integration/react"
import { persistor } from "./app/store.js"

ReactDOM.createRoot(document.getElementById("root")).render(
    <React.StrictMode>
        <BrowserRouter>
            <Provider store={store}>
                <PersistGate loading={null} persistor={persistor}>
                    <App />
                </PersistGate>
            </Provider>
        </BrowserRouter>
    </React.StrictMode>
)

Now everything should work. In the demo below, you can witness how easily we can add movies to the favorites list, remove them from the list, and access the entire favorites list. Most importantly, the state is persisted even after refreshing the page. Great job on completing this project 🎉

Please note that you might come across an error in the console that looks something like this:

I won't delve into the details of why it's happening, but you can easily avoid it by making these simple changes in the code when configuring the store:

export const store = configureStore({
    reducer: persistedReducer,
    middleware: (getDefaultMiddleware) => getDefaultMiddleware({
        serializableCheck: false
    })
})

Redux DevTools

Another recommendation I have is to install Redux DevTools. It's an awesome tool for debugging your state when working with Redux. You can easily install it as a browser extension from the provided link.

To convince you, here's a short demo showing how Redux DevTools work using our example. As you can see, not only can I track the current state of the app, but I can also inspect the entire timeline of actions and state changes within the application.

Conclusion

In this tutorial, we have covered the fundamentals of Redux, including how it works, updating and accessing the state, and achieving persistence using the movie list as an example. Additionally, I introduced you to the basics of working with React Bootstrap as well as to the exciting TMDb API, which you can freely use to build movie-related applications.

I hope you found this tutorial informative and gained new knowledge. Feel free to share your thoughts and let me know if you have any further questions 🚀

As usual, if you have any difficulties along the way, you can refer to the complete source code available on this CodeSandbox. Feel free to reach out to me, even if it's not related to this project 😃

If you enjoyed this article, remember to follow me on Twitter, where I share daily updates. And feel free to connect with me on LinkedIn.

By the way, this blog post was inspired by one of my previous projects that I worked on about six months ago. If you'd like to take a look, you can find the GitHub repository at the following link.