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.

building movie streaming app react tmdb api

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!