Building a Movie Streaming App with React and TMDB API
In this tutorial, we will walk through the process of building a movie streaming app using React and the TMDB API. React is a popular JavaScript library for building user interfaces, while the TMDB API provides access to a vast database of movies and TV shows. By combining these two technologies, we can create a powerful and dynamic movie streaming app.
Introduction
What is React?
React is a JavaScript library for building user interfaces. It allows developers to create reusable UI components and efficiently update the user interface when the underlying data changes. React uses a virtual DOM (Document Object Model) to efficiently update only the parts of the UI that have changed, resulting in a fast and responsive user experience.
What is TMDB API?
The TMDB API is a RESTful web service that provides access to a vast database of movies, TV shows, and other related content. It allows developers to retrieve information about specific movies, search for movies by various criteria, and get details about actors, directors, and other crew members.
Overview of the Movie Streaming App
The movie streaming app we will be building will allow users to browse and search for movies, view details about each movie, and play them in a built-in movie player. Users will also be able to sign up and log in to the app, allowing them to save their favorite movies and personalize their movie recommendations.
Setting Up the Project
Installing React
To get started, we need to install React. Open your terminal and run the following command:
npx create-react-app movie-streaming-app
This command will create a new directory called movie-streaming-app
and set up a new React project inside it.
Creating a TMDB API Key
Next, we need to create an account with TMDB and obtain an API key. Visit the TMDB website and sign up for an account. Once you have an account, navigate to the API section in your account settings and generate a new API key. Make sure to copy your API key as we will need it later to make requests to the TMDB API.
Initializing a React App
With React and the TMDB API key installed, we can now initialize our React app. Open your terminal, navigate to the project directory (movie-streaming-app
), and run the following command:
npm start
This command will start the development server and open the app in your default browser. You should see a basic React app with the "Welcome to React" message.
Designing the User Interface
Creating Component Structure
To create the component structure for our movie streaming app, we will start by creating a few basic components. In your project directory, navigate to the src
folder and create a new folder called components
. Inside the components
folder, create the following files:
Navbar.js
MovieList.js
MovieDetails.js
MoviePlayer.js
SignUp.js
Login.js
Open each file and add the following code:
// Navbar.js
import React from 'react';
const Navbar = () => {
return (
<nav>
{/* Navbar content */}
</nav>
);
};
export default Navbar;
// MovieList.js
import React from 'react';
const MovieList = () => {
return (
<div>
{/* Movie list content */}
</div>
);
};
export default MovieList;
// MovieDetails.js
import React from 'react';
const MovieDetails = () => {
return (
<div>
{/* Movie details content */}
</div>
);
};
export default MovieDetails;
// MoviePlayer.js
import React from 'react';
const MoviePlayer = () => {
return (
<div>
{/* Movie player content */}
</div>
);
};
export default MoviePlayer;
// SignUp.js
import React from 'react';
const SignUp = () => {
return (
<div>
{/* Sign up form */}
</div>
);
};
export default SignUp;
// Login.js
import React from 'react';
const Login = () => {
return (
<div>
{/* Login form */}
</div>
);
};
export default Login;
These are just placeholders for now. We will fill in the content of each component as we progress through the tutorial.
Styling with CSS
To style our movie streaming app, we will use CSS. In your project directory, navigate to the src
folder and create a new folder called styles
. Inside the styles
folder, create a new file called App.css
. Open App.css
and add the following CSS code:
/* App.css */
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
}
nav {
background-color: #333;
padding: 10px;
color: #fff;
}
.navbar-title {
font-size: 1.5rem;
margin: 0;
}
.movie-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-gap: 20px;
}
.movie-card {
background-color: #f9f9f9;
padding: 10px;
border-radius: 5px;
}
.movie-card img {
width: 100%;
height: auto;
border-radius: 5px;
}
This CSS code sets some basic styles for the app, including the font family, background color, and layout of the movie list. Feel free to customize these styles to match your desired design.
Implementing Navigation
To implement navigation in our movie streaming app, we will use React Router. React Router is a popular library for handling routing in React apps. To install React Router, open your terminal, navigate to the project directory (movie-streaming-app
), and run the following command:
npm install react-router-dom
Next, open src/App.js
and replace the existing code with the following:
// App.js
import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import Navbar from './components/Navbar';
import MovieList from './components/MovieList';
import MovieDetails from './components/MovieDetails';
import MoviePlayer from './components/MoviePlayer';
import SignUp from './components/SignUp';
import Login from './components/Login';
const App = () => {
return (
<Router>
<Navbar />
<Switch>
<Route exact path="/" component={MovieList} />
<Route path="/movie/:id" component={MovieDetails} />
<Route path="/player/:id" component={MoviePlayer} />
<Route path="/signup" component={SignUp} />
<Route path="/login" component={Login} />
</Switch>
</Router>
);
};
export default App;
In this code, we import the necessary components from React Router and our custom components. We then define the routes for our app using the Route
component. The exact
prop is used on the home route to ensure that it matches only the exact path. The path
prop specifies the URL path for each route, and the component
prop specifies the component to render for each route.
With navigation set up, we can now move on to fetching movie data from the TMDB API.
Fetching Movie Data
Making API Requests with Axios
To make API requests to the TMDB API, we will use Axios. Axios is a popular JavaScript library for making HTTP requests. To install Axios, open your terminal, navigate to the project directory (movie-streaming-app
), and run the following command:
npm install axios
Next, open src/components/MovieList.js
and replace the existing code with the following:
// MovieList.js
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const MovieList = () => {
const [movies, setMovies] = useState([]);
useEffect(() => {
const fetchMovies = async () => {
try {
const response = await axios.get(
`https://api.themoviedb.org/3/movie/popular?api_key=YOUR_API_KEY`
);
setMovies(response.data.results);
} catch (error) {
console.error(error);
}
};
fetchMovies();
}, []);
return (
<div className="movie-list">
{movies.map((movie) => (
<div key={movie.id} className="movie-card">
<img src={`https://image.tmdb.org/t/p/w500${movie.poster_path}`} alt={movie.title} />
<h3>{movie.title}</h3>
</div>
))}
</div>
);
};
export default MovieList;
In this code, we import Axios and use the useState
and useEffect
hooks from React. The useState
hook is used to manage the state of the movies array, while the useEffect
hook is used to fetch the movie data from the TMDB API when the component mounts.
Inside the useEffect
hook, we define an asynchronous function fetchMovies
that makes a GET request to the TMDB API. We pass the API key as a query parameter in the URL. The response from the API is then used to update the movies state using the setMovies
function.
In the JSX code, we map over the movies array and render a movie card for each movie. We use the movie's ID as the key and display the movie's poster image and title.
With this code, our movie list component will fetch and display a list of popular movies from the TMDB API. Next, let's implement search functionality to allow users to search for movies.
Implementing Search Functionality
To implement search functionality in our movie streaming app, we will add a search bar to the navbar and update the movie list based on the user's search query.
First, open src/components/Navbar.js
and replace the existing code with the following:
// Navbar.js
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
const Navbar = () => {
const [searchQuery, setSearchQuery] = useState('');
const handleSearch = (event) => {
event.preventDefault();
// TODO: Implement search functionality
};
return (
<nav>
<h1 className="navbar-title">
<Link to="/">Movie Streaming App</Link>
</h1>
<form onSubmit={handleSearch}>
<input
type="text"
placeholder="Search movies..."
value={searchQuery}
onChange={(event) => setSearchQuery(event.target.value)}
/>
<button type="submit">Search</button>
</form>
</nav>
);
};
export default Navbar;
In this code, we import the Link
component from React Router and use it to create a link to the home page (/
). We also define a form with an input field and a submit button. The value of the input field is controlled by the searchQuery
state, and the onChange
event updates the state as the user types.
We also define a handleSearch
function that is called when the form is submitted. For now, this function is empty, but we will implement the search functionality later.
Next, open src/components/MovieList.js
and update the code as follows:
// MovieList.js
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const MovieList = () => {
const [movies, setMovies] = useState([]);
const [searchQuery, setSearchQuery] = useState('');
useEffect(() => {
const fetchMovies = async () => {
try {
let url = `https://api.themoviedb.org/3/movie/popular?api_key=YOUR_API_KEY`;
if (searchQuery) {
url = `https://api.themoviedb.org/3/search/movie?api_key=YOUR_API_KEY&query=${searchQuery}`;
}
const response = await axios.get(url);
setMovies(response.data.results);
} catch (error) {
console.error(error);
}
};
fetchMovies();
}, [searchQuery]);
return (
<div className="movie-list">
{movies.map((movie) => (
<div key={movie.id} className="movie-card">
<img src={`https://image.tmdb.org/t/p/w500${movie.poster_path}`} alt={movie.title} />
<h3>{movie.title}</h3>
</div>
))}
</div>
);
};
export default MovieList;
In this code, we update the fetchMovies
function to conditionally build the URL based on the value of the searchQuery
state. If the searchQuery
is empty, we fetch popular movies, otherwise, we fetch movies based on the search query using the search/movie
endpoint of the TMDB API.
We also add the searchQuery
state to the dependency array of the useEffect
hook. This ensures that the effect is re-run whenever the searchQuery
changes, causing the movie list to be updated with the new search results.
With these changes, our movie list component will now update dynamically based on the user's search query. Next, let's build the movie player component.
Playing Movies
Building the Movie Player Component
To build the movie player component, we will create a new component called MoviePlayer
and add it to our app's routing. The MoviePlayer
component will display a video player and play the selected movie.
First, open src/components/MoviePlayer.js
and replace the existing code with the following:
// MoviePlayer.js
import React from 'react';
const MoviePlayer = () => {
return (
<div>
<video controls>
{/* Video source */}
</video>
</div>
);
};
export default MoviePlayer;
In this code, we create a video
element with the controls
attribute to enable the default video player controls. We will update the video source dynamically based on the selected movie.
Next, open src/App.js
and update the code as follows:
// App.js
import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import Navbar from './components/Navbar';
import MovieList from './components/MovieList';
import MovieDetails from './components/MovieDetails';
import MoviePlayer from './components/MoviePlayer';
import SignUp from './components/SignUp';
import Login from './components/Login';
const App = () => {
return (
<Router>
<Navbar />
<Switch>
<Route exact path="/" component={MovieList} />
<Route path="/movie/:id" component={MovieDetails} />
<Route path="/player/:id" component={MoviePlayer} />
<Route path="/signup" component={SignUp} />
<Route path="/login" component={Login} />
</Switch>
</Router>
);
};
export default App;
In this code, we import the MoviePlayer
component and add a new route for it. The :id
parameter in the route path allows us to pass the movie ID as a URL parameter.
With these changes, we can now navigate to the movie player component by visiting /player/:id
in our app's URL. Next, let's handle play and pause functionality in the movie player component.
Handling Play and Pause
To handle play and pause functionality in the movie player component, we will use the useRef
and useEffect
hooks from React. The useRef
hook allows us to create a mutable reference to the video element, while the useEffect
hook allows us to add event listeners to the video element and update the playback state.
Open src/components/MoviePlayer.js
and update the code as follows:
// MoviePlayer.js
import React, { useRef, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
const MoviePlayer = () => {
const { id } = useParams();
const videoRef = useRef(null);
const [isPlaying, setIsPlaying] = useState(false);
useEffect(() => {
const video = videoRef.current;
const handlePlay = () => {
setIsPlaying(true);
};
const handlePause = () => {
setIsPlaying(false);
};
video.addEventListener('play', handlePlay);
video.addEventListener('pause', handlePause);
return () => {
video.removeEventListener('play', handlePlay);
video.removeEventListener('pause', handlePause);
};
}, []);
return (
<div>
<video ref={videoRef} controls>
<source
src={`https://api.themoviedb.org/3/movie/${id}/videos?api_key=YOUR_API_KEY`}
type="video/mp4"
/>
</video>
<button onClick={() => { isPlaying ? videoRef.current.pause() : videoRef.current.play(); }}>
{isPlaying ? 'Pause' : 'Play'}
</button>
</div>
);
};
export default MoviePlayer;
In this code, we import the useParams
hook from React Router to access the movie ID from the URL parameters. We also use the useRef
and useEffect
hooks to create a reference to the video element and add event listeners to it.
Inside the useEffect
hook, we define two event handler functions, handlePlay
and handlePause
, that update the isPlaying
state based on the playback events of the video element. We add the event listeners to the video element and return a cleanup function that removes the event listeners when the component unmounts.
In the JSX code, we set the ref
attribute of the video element to the videoRef
reference. We also update the video source URL dynamically based on the movie ID. Finally, we add a play/pause button that toggles the playback state of the video element when clicked.
With these changes, our movie player component will now play and pause the selected movie. Next, let's add support for subtitles and captions.
Adding Subtitles and Captions
To add support for subtitles and captions in the movie player component, we can use the track
element within the video
element. The track
element allows us to specify the URL of a WebVTT file that contains the subtitles or captions for the video.
First, make sure you have a WebVTT file with subtitles or captions for your video. You can create a WebVTT file using a text editor and save it with a .vtt
extension. The content of the file should follow the WebVTT format, which consists of timestamped cues and their corresponding text.
Next, open src/components/MoviePlayer.js
and update the code as follows:
// MoviePlayer.js
import React, { useRef, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
const MoviePlayer = () => {
const { id } = useParams();
const videoRef = useRef(null);
const [isPlaying, setIsPlaying] = useState(false);
useEffect(() => {
const video = videoRef.current;
const handlePlay = () => {
setIsPlaying(true);
};
const handlePause = () => {
setIsPlaying(false);
};
video.addEventListener('play', handlePlay);
video.addEventListener('pause', handlePause);
return () => {
video.removeEventListener('play', handlePlay);
video.removeEventListener('pause', handlePause);
};
}, []);
return (
<div>
<video ref={videoRef} controls>
<source
src={`https://api.themoviedb.org/3/movie/${id}/videos?api_key=YOUR_API_KEY`}
type="video/mp4"
/>
<track
src="/path/to/subtitles.vtt"
kind="subtitles"
srcLang="en"
label="English"
default
/>
</video>
<button onClick={() => { isPlaying ? videoRef.current.pause() : videoRef.current.play(); }}>
{isPlaying ? 'Pause' : 'Play'}
</button>
</div>
);
};
export default MoviePlayer;
In this code, we add a track
element inside the video
element. The src
attribute of the track
element should be set to the URL of your WebVTT file. The kind
attribute can be set to subtitles
or captions
depending on the type of text track. The srcLang
attribute specifies the language of the subtitles or captions, while the label
attribute specifies the label to be displayed in the user interface. The default
attribute is used to specify the default text track.
With these changes, our movie player component will now display the subtitles or captions specified in the WebVTT file. Next, let's implement user authentication.
User Authentication
Implementing Sign Up and Login
To implement user authentication in our movie streaming app, we will create two new components: SignUp
and Login
. The SignUp
component will allow users to create a new account, while the Login
component will allow users to log in to their existing account.
First, open src/components/SignUp.js
and replace the existing code with the following:
// SignUp.js
import React, { useState } from 'react';
const SignUp = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const handleSubmit = (event) => {
event.preventDefault();
// TODO: Implement sign up functionality
};
return (
<div>
<h2>Sign Up</h2>
<form onSubmit={handleSubmit}>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
/>
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
/>
<label htmlFor="confirmPassword">Confirm Password:</label>
<input
type="password"
id="confirmPassword"
value={confirmPassword}
onChange={(event) => setConfirmPassword(event.target.value)}
/>
<button type="submit">Sign Up</button>
</form>
</div>
);
};
export default SignUp;
In this code, we use the useState
hook to manage the state of the email, password, and confirmPassword fields. The handleSubmit
function is called when the form is submitted. For now, this function is empty, but we will implement the sign up functionality later.
Next, open src/components/Login.js
and replace the existing code with the following:
// Login.js
import React, { useState } from 'react';
const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (event) => {
event.preventDefault();
// TODO: Implement login functionality
};
return (
<div>
<h2>Login</h2>
<form onSubmit={handleSubmit}>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
/>
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
/>
<button type="submit">Login</button>
</form>
</div>
);
};
export default Login;
In this code, we use the useState
hook to manage the state of the email and password fields. The handleSubmit
function is called when the form is submitted. For now, this function is empty, but we will implement the login functionality later.
Next, open src/App.js
and update the code as follows:
// App.js
import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import Navbar from './components/Navbar';
import MovieList from './components/MovieList';
import MovieDetails from './components/MovieDetails';
import MoviePlayer from './components/MoviePlayer';
import SignUp from './components/SignUp';
import Login from './components/Login';
const App = () => {
return (
<Router>
<Navbar />
<Switch>
<Route exact path="/" component={MovieList} />
<Route path="/movie/:id" component={MovieDetails} />
<Route path="/player/:id" component={MoviePlayer} />
<Route path="/signup" component={SignUp} />
<Route path="/login" component={Login} />
</Switch>
</Router>
);
};
export default App;
In this code, we import the SignUp
and Login
components and add new routes for them. The /signup
route will render the SignUp
component, while the /login
route will render the Login
component.
With these changes, our movie streaming app now has sign up and login functionality. Next, let's secure our API requests and manage user sessions.
Securing API Requests
To secure our API requests, we can use the Authorization
header to include an access token or session token with each request. This token can be obtained after a successful login or signup.
First, open src/components/Login.js
and update the code as follows:
// Login.js
import React, { useState } from 'react';
import axios from 'axios';
const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = async (event) => {
event.preventDefault();
try {
const response = await axios.post('/api/login', { email, password });
const { token } = response.data;
// TODO: Store the token in local storage or a secure cookie
// Redirect the user to the home page
window.location.href = '/';
} catch (error) {
console.error(error);
}
};
return (
<div>
<h2>Login</h2>
<form onSubmit={handleSubmit}>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
/>
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
/>
<button type="submit">Login</button>
</form>
</div>
);
};
export default Login;
In this code, we import Axios and use it to make a POST request to the /api/login
endpoint. We pass the email and password as the request body. If the login is successful, we extract the token from the response and store it in local storage or a secure cookie. We then redirect the user to the home page.
Next, open src/components/SignUp.js
and update the code as follows:
// SignUp.js
import React, { useState } from 'react';
import axios from 'axios';
const SignUp = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const handleSubmit = async (event) => {
event.preventDefault();
if (password !== confirmPassword) {
console.error('Passwords do not match');
return;
}
try {
await axios.post('/api/signup', { email, password });
// Redirect the user to the login page
window.location.href = '/login';
} catch (error) {
console.error(error);
}
};
return (
<div>
<h2>Sign Up</h2>
<form onSubmit={handleSubmit}>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
/>
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
/>
<label htmlFor="confirmPassword">Confirm Password:</label>
<input
type="password"
id="confirmPassword"
value={confirmPassword}
onChange={(event) => setConfirmPassword(event.target.value)}
/>
<button type="submit">Sign Up</button>
</form>
</div>
);
};
export default SignUp;
In this code, we import Axios and use it to make a POST request to the /api/signup
endpoint. We pass the email and password as the request body. If the signup is successful, we redirect the user to the login page.
With these changes, our movie streaming app now secures the login and signup requests. Next, let's manage user sessions.
Managing User Sessions
To manage user sessions in our movie streaming app, we can use a server-side solution such as JSON Web Tokens (JWT) or session cookies. In this tutorial, we will use JWT.
First, install the necessary dependencies by running the following command in your terminal:
npm install jsonwebtoken express express-validator
Next, create a new file called server.js
in the root directory of your project and add the following code:
// server.js
const express = require('express');
const { body, validationResult } = require('express-validator');
const jwt = require('jsonwebtoken');
const app = express();
app.use(express.json());
const JWT_SECRET = 'your-secret-key';
app.post(
'/api/signup',
[
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 6 }),
],
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { email, password } = req.body;
// TODO: Implement the signup logic
const token = jwt.sign({ email }, JWT_SECRET);
res.json({ token });
}
);
app.post(
'/api/login',
[
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 6 }),
],
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { email, password } = req.body;
// TODO: Implement the login logic
const token = jwt.sign({ email }, JWT_SECRET);
res.json({ token });
}
);
app.listen(5000, () => {
console.log('Server listening on port 5000');
});
In this code, we import the necessary modules and create a new Express app. We use the express.json()
middleware to parse JSON request bodies.
We define two route handlers for the /api/signup
and /api/login
endpoints. Inside each route handler, we use the express-validator
module to validate the email and password fields. If there are any validation errors, we respond with a 400 status code and the array of errors. Otherwise, we sign a JWT with the user's email and send it back in the response.
Make sure to replace 'your-secret-key'
with a secret key of your choice. This key should be kept secret and not shared publicly.
To start the server, open your terminal and run the following command:
node server.js
With these changes, our movie streaming app now manages user sessions using JWT. Next, let's conclude the tutorial.
Conclusion
In this tutorial, we have built a movie streaming app using React and the TMDB API. We started by setting up the project and designing the user interface. We then fetched movie data from the TMDB API, implemented search functionality, and added a movie player component. We also implemented user authentication, secured our API requests, and managed user sessions using JWT.
By following this tutorial, you have learned how to create a movie streaming app with React and integrate it with a powerful movie database API. You have also gained knowledge on how to implement user authentication and secure API requests in a React app.
Feel free to explore and expand upon this project by adding more features, such as user profiles, movie recommendations, and personalized playlists. Happy coding!