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.
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!