Building a Movie Recommendation App with React and TMDB API

In this tutorial, we will learn how to build a movie recommendation app using React and the TMDB API. A movie recommendation app is a web application that suggests movies to users based on their preferences and ratings. We will use React, a popular JavaScript library for building user interfaces, to create a dynamic and interactive app. The TMDB API will provide us with the necessary data about movies, including details and ratings.

building movie recommendation app react tmdb api

Introduction

What is a movie recommendation app?

A movie recommendation app is a web application that suggests movies to users based on their preferences and ratings. It helps users discover new movies that they might enjoy based on their previous interactions with the app.

Why use React for building the app?

React is a powerful JavaScript library for building user interfaces. It allows us to create reusable UI components and efficiently update the user interface when the underlying data changes. React's virtual DOM ensures optimal performance, making it an ideal choice for building dynamic and interactive applications like a movie recommendation app.

What is the TMDB API?

The TMDB API is a popular API that provides access to a vast database of movies and TV shows. It allows developers to retrieve information about movies, including details, ratings, and images. We will use the TMDB API to fetch movie data for our recommendation app.

Setting Up the Project

Creating a new React project

To start building our movie recommendation app, we need to set up a new React project. We can use Create React App, a command-line tool, to quickly create a new React project with all the necessary dependencies and configurations.

npx create-react-app movie-recommendation-app
cd movie-recommendation-app

Installing necessary dependencies

After creating the React project, we need to install some additional dependencies. We will use axios for making HTTP requests to the TMDB API and react-router-dom for managing the app's routing.

npm install axios react-router-dom

Obtaining an API key for TMDB

To access the TMDB API, we need to obtain an API key. We can sign up for a free account on the TMDB website and generate an API key. The API key will be used to authenticate our requests to the TMDB API.

Building the Movie Search Component

Creating the search form

The movie search component will allow users to search for movies based on their titles. We will create a form with an input field for users to enter the movie title and a submit button to initiate the search.

import React, { useState } from 'react';

function MovieSearch() {
  const [searchQuery, setSearchQuery] = useState('');

  const handleInputChange = (event) => {
    setSearchQuery(event.target.value);
  };

  const handleSubmit = (event) => {
    event.preventDefault();
    // TODO: Fetch movie data from TMDB API based on searchQuery
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" value={searchQuery} onChange={handleInputChange} />
      <button type="submit">Search</button>
    </form>
  );
}

export default MovieSearch;

In the above code, we define a functional component called MovieSearch. It uses the useState hook to create a state variable searchQuery and a setter function setSearchQuery to update the state. The handleInputChange function is called whenever the input field value changes, updating the searchQuery state accordingly. The handleSubmit function is called when the form is submitted, preventing the default form submission behavior and initiating the movie search.

Handling user input

To fetch movie data from the TMDB API, we need to send a GET request to the appropriate endpoint with the search query as a parameter. We will use axios, a popular HTTP client library, to make the request.

import axios from 'axios';

const apiKey = 'YOUR_TMDB_API_KEY';

const fetchMovies = async (searchQuery) => {
  try {
    const response = await axios.get(
      `https://api.themoviedb.org/3/search/movie?api_key=${apiKey}&query=${searchQuery}`
    );
    const movies = response.data.results;
    // TODO: Handle the fetched movies data
  } catch (error) {
    console.error('Error fetching movies:', error);
  }
};

In the above code, we define an apiKey variable with your TMDB API key. The fetchMovies function takes a searchQuery parameter and uses axios to send a GET request to the TMDB API's search endpoint. The response contains an array of movie objects, which we can handle in the TODO section based on our app's requirements.

Fetching movie data from TMDB

To fetch movie data from the TMDB API, we need to call the fetchMovies function with the searchQuery as an argument. We can do this inside the handleSubmit function of the MovieSearch component.

import React, { useState } from 'react';
import axios from 'axios';

const apiKey = 'YOUR_TMDB_API_KEY';

function MovieSearch() {
  const [searchQuery, setSearchQuery] = useState('');
  const [movies, setMovies] = useState([]);

  const handleInputChange = (event) => {
    setSearchQuery(event.target.value);
  };

  const handleSubmit = async (event) => {
    event.preventDefault();
    try {
      const response = await fetchMovies(searchQuery);
      setMovies(response.data.results);
    } catch (error) {
      console.error('Error fetching movies:', error);
    }
  };

  const fetchMovies = async (searchQuery) => {
    try {
      const response = await axios.get(
        `https://api.themoviedb.org/3/search/movie?api_key=${apiKey}&query=${searchQuery}`
      );
      return response.data;
    } catch (error) {
      console.error('Error fetching movies:', error);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" value={searchQuery} onChange={handleInputChange} />
      <button type="submit">Search</button>
      {/* TODO: Display the fetched movies */}
    </form>
  );
}

export default MovieSearch;

In the above code, we define a state variable movies using the useState hook. Inside the handleSubmit function, we call the fetchMovies function and update the movies state with the fetched movie data. We can then use the movies state to display the fetched movies in the TODO section of the code.

Displaying Movie Recommendations

Designing the recommendation layout

To display movie recommendations to users, we need to design a suitable layout. We can use CSS to style the recommendations and display them in a visually appealing manner. For simplicity, we will render the movie posters and details in a grid layout.

import React from 'react';

function MovieRecommendations({ movies }) {
  return (
    <div className="movie-recommendations">
      {movies.map((movie) => (
        <div key={movie.id} className="movie-recommendation">
          <img
            src={`https://image.tmdb.org/t/p/w500/${movie.poster_path}`}
            alt={movie.title}
          />
          <h3>{movie.title}</h3>
          <p>{movie.overview}</p>
          {/* TODO: Implement user ratings and feedback */}
        </div>
      ))}
    </div>
  );
}

export default MovieRecommendations;

In the above code, we define a functional component called MovieRecommendations. It takes a movies prop, which should be an array of movie objects. Inside the component, we use the map function to iterate over the movies array and render each movie's poster, title, and overview. We can add additional components or functionality in the TODO section of the code.

Rendering movie posters and details

To display the movie recommendations, we need to pass the fetched movie data to the MovieRecommendations component. We can do this inside the MovieSearch component by rendering the MovieRecommendations component and passing the movies state as a prop.

import React, { useState } from 'react';
import axios from 'axios';
import MovieRecommendations from './MovieRecommendations';

const apiKey = 'YOUR_TMDB_API_KEY';

function MovieSearch() {
  const [searchQuery, setSearchQuery] = useState('');
  const [movies, setMovies] = useState([]);

  const handleInputChange = (event) => {
    setSearchQuery(event.target.value);
  };

  const handleSubmit = async (event) => {
    event.preventDefault();
    try {
      const response = await fetchMovies(searchQuery);
      setMovies(response.data.results);
    } catch (error) {
      console.error('Error fetching movies:', error);
    }
  };

  const fetchMovies = async (searchQuery) => {
    try {
      const response = await axios.get(
        `https://api.themoviedb.org/3/search/movie?api_key=${apiKey}&query=${searchQuery}`
      );
      return response.data;
    } catch (error) {
      console.error('Error fetching movies:', error);
    }
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input type="text" value={searchQuery} onChange={handleInputChange} />
        <button type="submit">Search</button>
      </form>
      <MovieRecommendations movies={movies} />
    </div>
  );
}

export default MovieSearch;

In the above code, we import the MovieRecommendations component and render it below the search form. We pass the movies state as a prop to the MovieRecommendations component, allowing it to access the fetched movie data.

Implementing user ratings and feedback

To allow users to rate movies and provide feedback, we can add additional UI components and functionality. For example, we can render a star rating component and display user feedback based on their interactions with the movie recommendations.

import React from 'react';

function MovieRecommendations({ movies }) {
  const handleRatingChange = (movieId, rating) => {
    // TODO: Implement rating logic
  };

  return (
    <div className="movie-recommendations">
      {movies.map((movie) => (
        <div key={movie.id} className="movie-recommendation">
          <img
            src={`https://image.tmdb.org/t/p/w500/${movie.poster_path}`}
            alt={movie.title}
          />
          <h3>{movie.title}</h3>
          <p>{movie.overview}</p>
          <div className="rating">
            <StarRating
              value={movie.rating}
              onChange={(rating) => handleRatingChange(movie.id, rating)}
            />
          </div>
          <div className="feedback">
            {/* TODO: Display user feedback */}
          </div>
        </div>
      ))}
    </div>
  );
}

export default MovieRecommendations;

In the above code, we define a handleRatingChange function that takes a movieId and rating as parameters. This function can be called when the user interacts with the star rating component, allowing us to update the movie's rating in our app's state or send the user's rating to the server for further processing. We can also add additional UI components or functionality in the TODO section of the code.

Adding User Authentication

Setting up user registration and login

To add user authentication to our movie recommendation app, we need to implement user registration and login functionality. This can be done using a backend server and a database to store user information securely. For simplicity, we will focus on the frontend implementation and simulate user authentication using localStorage.

import React, { useState } from 'react';

function UserAuthentication() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');

  const handleUsernameChange = (event) => {
    setUsername(event.target.value);
  };

  const handlePasswordChange = (event) => {
    setPassword(event.target.value);
  };

  const handleLogin = (event) => {
    event.preventDefault();
    // TODO: Implement login logic
  };

  const handleLogout = () => {
    // TODO: Implement logout logic
  };

  if (isLoggedIn) {
    return (
      <div>
        <p>Welcome, {username}!</p>
        <button onClick={handleLogout}>Logout</button>
      </div>
    );
  }

  return (
    <form onSubmit={handleLogin}>
      <input
        type="text"
        value={username}
        onChange={handleUsernameChange}
        placeholder="Username"
      />
      <input
        type="password"
        value={password}
        onChange={handlePasswordChange}
        placeholder="Password"
      />
      <button type="submit">Login</button>
    </form>
  );
}

export default UserAuthentication;

In the above code, we define a functional component called UserAuthentication. It uses the useState hook to manage the isLoggedIn, username, and password states. The handleUsernameChange and handlePasswordChange functions update the respective state variables when the input fields change. The handleLogin function is called when the login form is submitted, preventing the default form submission behavior and implementing the login logic. We can add additional functionality in the TODO sections of the code.

Securing API requests with authentication

To secure our API requests, we need to include the user's authentication token or API key in the request headers. We can store the authentication token or API key in localStorage and retrieve it when making API requests.

import axios from 'axios';

const apiKey = 'YOUR_TMDB_API_KEY';
const authToken = localStorage.getItem('authToken');

const fetchMovies = async (searchQuery) => {
  try {
    const response = await axios.get(
      `https://api.themoviedb.org/3/search/movie?api_key=${apiKey}&query=${searchQuery}`,
      {
        headers: {
          Authorization: `Bearer ${authToken}`,
        },
      }
    );
    return response.data;
  } catch (error) {
    console.error('Error fetching movies:', error);
  }
};

In the above code, we define an authToken variable that retrieves the authentication token from localStorage. We include the authToken in the request headers using the Authorization header field. This ensures that our requests are authenticated and secure.

Storing user preferences and recommendations

To store user preferences and recommendations, we can use localStorage or a backend server with a database. For simplicity, we will use localStorage to store the user's preferences and recommendations as an array of movie IDs.

const handleRatingChange = (movieId, rating) => {
  // TODO: Update user preferences and recommendations
  const preferences = JSON.parse(localStorage.getItem('userPreferences')) || {};
  preferences[movieId] = rating;
  localStorage.setItem('userPreferences', JSON.stringify(preferences));
};

In the above code, we retrieve the user's preferences from localStorage and update the preferences with the movie ID and rating. We then store the updated preferences back into localStorage. We can implement similar logic to store and retrieve user recommendations.

Improving Performance and User Experience

Implementing caching and memoization

To improve performance and reduce unnecessary API requests, we can implement caching and memoization techniques. We can store the fetched movie data in a cache object and check if the data is already available before making a new API request.

import React, { useState, useEffect } from 'react';
import axios from 'axios';

const apiKey = 'YOUR_TMDB_API_KEY';
const cache = {};

function MovieSearch() {
  const [searchQuery, setSearchQuery] = useState('');
  const [movies, setMovies] = useState([]);

  useEffect(() => {
    if (searchQuery) {
      fetchMovies(searchQuery);
    }
  }, [searchQuery]);

  const handleInputChange = (event) => {
    setSearchQuery(event.target.value);
  };

  const fetchMovies = async (searchQuery) => {
    if (cache[searchQuery]) {
      setMovies(cache[searchQuery]);
    } else {
      try {
        const response = await axios.get(
          `https://api.themoviedb.org/3/search/movie?api_key=${apiKey}&query=${searchQuery}`
        );
        const fetchedMovies = response.data.results;
        cache[searchQuery] = fetchedMovies;
        setMovies(fetchedMovies);
      } catch (error) {
        console.error('Error fetching movies:', error);
      }
    }
  };

  return (
    <form>
      <input type="text" value={searchQuery} onChange={handleInputChange} />
    </form>
  );
}

export default MovieSearch;

In the above code, we define a cache object to store the fetched movie data. Inside the fetchMovies function, we check if the requested movie data is already available in the cache. If it is, we set the movies state with the cached data. Otherwise, we make a new API request and update the cache and movies state with the fetched movie data.

Optimizing network requests

To optimize network requests, we can implement techniques like debouncing and throttling. Debouncing delays the execution of a function until a certain period of inactivity has passed, while throttling limits the rate at which a function can be called.

import React, { useState, useEffect } from 'react';
import axios from 'axios';

const apiKey = 'YOUR_TMDB_API_KEY';
const cache = {};
let timeout;

function MovieSearch() {
  const [searchQuery, setSearchQuery] = useState('');
  const [movies, setMovies] = useState([]);

  useEffect(() => {
    if (searchQuery) {
      clearTimeout(timeout);
      timeout = setTimeout(() => fetchMovies(searchQuery), 500);
    }
  }, [searchQuery]);

  const handleInputChange = (event) => {
    setSearchQuery(event.target.value);
  };

  const fetchMovies = async (searchQuery) => {
    if (cache[searchQuery]) {
      setMovies(cache[searchQuery]);
    } else {
      try {
        const response = await axios.get(
          `https://api.themoviedb.org/3/search/movie?api_key=${apiKey}&query=${searchQuery}`
        );
        const fetchedMovies = response.data.results;
        cache[searchQuery] = fetchedMovies;
        setMovies(fetchedMovies);
      } catch (error) {
        console.error('Error fetching movies:', error);
      }
    }
  };

  return (
    <form>
      <input type="text" value={searchQuery} onChange={handleInputChange} />
    </form>
  );
}

export default MovieSearch;

In the above code, we use the setTimeout function to delay the execution of the fetchMovies function by 500 milliseconds. This introduces a small delay before making the API request, allowing the user to finish typing their query before sending the request. We can adjust the delay time based on our app's requirements.

Adding loading spinners and error handling

To provide a better user experience, we can add loading spinners and error handling to our movie recommendation app. Loading spinners indicate that a request is in progress, while error handling displays an error message when a request fails.

import React, { useState, useEffect } from 'react';
import axios from 'axios';

const apiKey = 'YOUR_TMDB_API_KEY';
const cache = {};
let timeout;

function MovieSearch() {
  const [searchQuery, setSearchQuery] = useState('');
  const [movies, setMovies] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    if (searchQuery) {
      clearTimeout(timeout);
      timeout = setTimeout(() => fetchMovies(searchQuery), 500);
    }
  }, [searchQuery]);

  const handleInputChange = (event) => {
    setSearchQuery(event.target.value);
  };

  const fetchMovies = async (searchQuery) => {
    if (cache[searchQuery]) {
      setMovies(cache[searchQuery]);
    } else {
      setIsLoading(true);
      setError(null);
      try {
        const response = await axios.get(
          `https://api.themoviedb.org/3/search/movie?api_key=${apiKey}&query=${searchQuery}`
        );
        const fetchedMovies = response.data.results;
        cache[searchQuery] = fetchedMovies;
        setMovies(fetchedMovies);
      } catch (error) {
        setError('Error fetching movies. Please try again.');
      } finally {
        setIsLoading(false);
      }
    }
  };

  return (
    <form>
      <input type="text" value={searchQuery} onChange={handleInputChange} />
      {isLoading && <div>Loading...</div>}
      {error && <div>{error}</div>}
    </form>
  );
}

export default MovieSearch;

In the above code, we define isLoading and error states to track the loading state and any errors that occur during the API request. We set isLoading to true before making the request and false after the request is completed. We also set error to any error message that occurs during the request. We can then conditionally render loading spinners and error messages based on the state values.

Conclusion

In this tutorial, we learned how to build a movie recommendation app using React and the TMDB API. We started by setting up the project and installing the necessary dependencies. We then built the movie search component, allowing users to search for movies based on their titles. Next, we designed the recommendation layout and displayed movie posters and details. We also added user authentication, allowing users to register and login to the app. Additionally, we discussed techniques for improving performance and user experience, including caching and memoization, optimizing network requests, and adding loading spinners and error handling. By following this tutorial, you should now have a good understanding of how to build a movie recommendation app with React and the TMDB API. Happy coding!