Notes from Build a YouTube Clone Application Using React
Repository with code available at redconfetti/react-youtube-clone
The following notes are for Mac users. You’ll need to use some commands specific to your system as needed.
Setup
VSCode
This course uses VSCode to demonstrate all examples. You can enable the terminal within VSCode you can use CTRL + ` (backtick), or use the menu - View > Terminal.
Create-React-App
You need to have the create-react-app tool installed to use from the command line, which requires that you have NodeJS installed.
$ npm i -g create-react-app
$ which create-react-app
/usr/local/bin/create-react-app
$ cd Projects
$ mkdir youtube-api
$ cd youtube-api
$ create-react-app ./
$ npm install
Install Dependencies
Once the script finishes, we’ll install our dependencies.
npm install --save axios @material-ui/core
Material UI
Material-UI provides pre-styled React components for you to use, similar to how Bootstrap provides UI elements out of the box for new projects. It follows the patterns of [Material Design].
It has a container system, grid system, buttons, etc.
Start the Dev Server
npm start
Remove Source Folder
We’re going to start over by removing the src
folder and recreate it with
our files from scratch.
rm -rf src
mkdir src
cd src
touch index.js
touch App.js
mkdir components
mkdir api
Create Index and App
// src/index.js
import React from "react"
import ReactDOM from "react-dom"
import App from "./App"
ReactDOM.render(<App />, document.querySelector("#root"))
// src/app.js
import React from "react"
class App extends React.Component {
render() {
return <h1>YouTube Clone App</h1>
}
}
export default App
Here we create our App class as a Class based component. Another type of component you can create is a functional component.
Developers use class based components usually if there is any complexity to the component (“smart components”). Class components support lifecycle methods, and can manage the state.
Functional components, also known as “Dummy components”, are basic JavaScript functions that process the input and return the component to be rendered.
The above class component could be rewritten using the code below, however this won’t have the same level of support as a class based component.
const App = () => {
return <h1>YouTube Clone App</h1>
}
App Binding to Root Element
In our public/index.html
file you’ll see that in the body of the page there
is a DIV defined with id of “root”. All of our application will be rendered
within this root division.
<!DOCTYPE html>
<html lang="en">
<head>
<!-- ... -->
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
There is no need to modify this HTML file from this point forward. Everything
will be defined within the src
folder moving forward.
API Access
Under the ‘src/api’ folder, create a file called youtube.js
.
This is where we’re going to define our function that gets data from the YouTube API. You will need a Google Account to access the API console, from which you will obtain an API key.
You’ll have to setup a new project, then choose ‘Library’ and search for the YouTube Data API v3. Choose ‘Enable’ and proceed to setup the credential. Make sure to choose that you’ll be using it from a Web browser (JavaScript), and that it will be accessing ‘Public data’.
We’re using Axios here to configure the API settings which include the key provided by Google to access the YouTube Data API v3.
// src/api/youtube.js
export default axios.create({
baseURL: "https://www.googleapis.com/youtube/v3",
params: {
part: "snippet",
maxResults: 5,
key: "abcdEFGHijklmNOPQrstuvWXYZabcdEFGHdPpLk"
}
})
The Basics of our Application
Let’s import the Grid component that we’re going to use from Material-UI Core, and also our YouTube API request object.
// src/App.js
import React from "react"
import { Grid } from "@material-ui/core"
import youtube from "./api/youtube"
class App extends React.Component {
render() {
return <h1>YouTube Clone App</h1>
}
}
export default App
Next we’re going to update our App component so that it uses the Grid container.
Here you see we’ve created our main container using the full 16 spaces. Inside of it we’ve created an item using only 12 spaces. This establishes the main area where content shows with a margin of 2 spaces on the left and right side.
Within this there is yet another container defined to represent our main space.
It has the search bar at the top using 12 spaces, with the video details and video list items displayed beneath it.
// src/App.js
import React from "react"
import { Grid } from "@material-ui/core"
import youtube from "./api/youtube"
class App extends React.Component {
render() {
return (
<Grid justify="center" container spacing={10}>
<Grid item xs={12}>
<Grid container spacing={10}>
<Grid item xs={12}>
{/* SEARCH BAR */}
</Grid>
<Grid item xs={8}>
{/* VIDEO DETAILS*/}
</Grid>
<Grid item xs={4}>
{/* VIDEO LIST */}
</Grid>
</Grid>
</Grid>
</Grid>
)
}
}
export default App
Note: We’re using inline CSS for this tutorial. This obviously isn’t recommended for real projects, but works for this demonstration.
Our Components
Let’s add an import statement for the components we’re about to create.
import SearchBar from "./components/SearchBar"
import VideoDetail from "./components/VideoDetail"
// import VideoList from "./components/VideoList"
mkdir -p src/components
touch src/components/SearchBar.js
touch src/components/VideoList.js
touch src/components/VideoDetail.js
Search Bar Component
We’re using a class based component because the state will be used.
// src/components/SearchBar.js
import React from "react"
class SearchBar extends React.Component {
state = {
searchTerm: ""
}
render() {
return <h1>This is a search bar</h1>
}
}
export default SearchBar
Video Detail Component
// src/components/VideoDetail.js
import React from "react"
const VideoDetail = () => {
return <h1>This is a Video Detail component</h1>
}
export default VideoDetail
Now that we’ve established the basic boilerplate for these components, let’s add
them to our App.js. Because we’re not putting anything within these components,
we add them using the self-closing XML syntax (<SearchBar />
, <VideoDetail />
).
// src/App.js
import React from "react"
import { Grid } from "@material-ui/core"
import youtube from "./api/youtube"
class App extends React.Component {
render() {
return (
<Grid justify="center" container spacing={16}>
<Grid item xs={12}>
<Grid container spacing={16}>
<Grid item xs={12}>
<SearchBar />
</Grid>
<Grid item xs={8}>
<VideoDetail />
</Grid>
<Grid item xs={4}>
{/* VIDEO LIST */}
</Grid>
</Grid>
</Grid>
</Grid>
)
}
}
export default App
Creating a Component Index
If you’d like to define your components separately, but import them all at once,
you can create an index.js
file in src/components
that exports them
individually.
// src/components/index.js
export { default as SearchBar } from "./SearchBar"
export { default as VideoDetail } from "./VideoDetail"
Now we can redefine our import in src/App.js
like so:
import { SearchBar, VideoDetail } from "./components"
Building the Search Bar
Let’s import the components we’re going to use from the Material-UI library.
// src/components/SearchBar.js
import React from "react"
import { Paper, TextField } from "@material-ui/core"
class SearchBar extends React.Component {
state = {
searchTerm: ""
}
render() {
return (
<Paper elevation={6} style={{ padding: "25px" }}>
<form>
<TextField fullWidth label="Search..."></TextField>
</form>
</Paper>
)
}
}
export default SearchBar
If you check in your browser, you’ll have a nice long search bar at the top.
Search Bar Event Handlers
Now we want to add an event handler to the form that is executed when the search
is submitted (<form onSubmit={this.handleSubmit}>
).
// src/components/SearchBar.js
import React from "react"
import { Paper, TextField } from "@material-ui/core"
class SearchBar extends React.Component {
state = {
searchTerm: ""
}
render() {
return (
<Paper elevation={6} style={{ padding: "25px" }}>
<form onSubmit={this.handleSubmit}>
<TextField fullWidth label="Search..."></TextField>
</form>
</Paper>
)
}
}
export default SearchBar
We can also add an ‘onChange’ method to the TextField. This will handle input changes to the text field.
// src/components/SearchBar.js
import React from "react"
import { Paper, TextField } from "@material-ui/core"
class SearchBar extends React.Component {
state = {
searchTerm: ""
}
render() {
return (
<Paper elevation={6} style={{ padding: "25px" }}>
<form onSubmit={this.handleSubmit}>
<TextField
fullWidth
label="Search..."
onChange={this.handleChange}
></TextField>
</form>
</Paper>
)
}
}
export default SearchBar
Binding our Event Handling Functions
Within the React Docs for Handling Events the example shows a function declared within the class as per the normal method.
When a normal function is declared like this, the function has it’s own scope
where this
refers to the function itself. This is why there is a call to bind
the class to this
within the constructor.
class Toggle extends React.Component {
constructor(props) {
super(props)
this.state = { isToggleOn: true }
// This binding is necessary to make `this` work in the callback
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
this.setState(state => ({
isToggleOn: !state.isToggleOn
}))
}
render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? "ON" : "OFF"}
</button>
)
}
}
There is a simple work-around to this issue. You can simply declare the function
using an arrow-function, as they do not have their own this
defined in their
scope.
handleChange = event => {
this.setState({
searchTerm: event.target.value
})
}
We can also write this in a single line.
handleChange = event => this.setState({ searchTerm: event.target.value })
Now we can also add our handleSubmit
function also. As you can see this
makes use of the destructuring assignment syntax added by ES6 to define
a constant called searchTerm
from the searchTerm
property of this.state
.
// src/components/SearchBar.js
import React from "react"
import { Paper, TextField } from "@material-ui/core"
class SearchBar extends React.Component {
state = {
searchTerm: ""
}
handleChange = event => this.setState({ searchTerm: event.target.value })
handleSubmit = () => {
const { searchTerm } = this.state
}
render() {
return (
<Paper elevation={6} style={{ padding: "25px" }}>
<form onSubmit={this.handleSubmit}>
<TextField
fullWidth
label="Search..."
onChange={this.handleChange}
></TextField>
</form>
</Paper>
)
}
}
export default SearchBar
To make the searchTerm string available to other components, we need to pass in
a function via a prop called onFormSubmit
.
// src/App.js
// ...
<SearchBar onFormSubmit={this.handleSubmit} />
// ...
Within our SearchBar components we can then update our handleSubmit
function
so that it is able to call this function.
// src/components/SearchBar.js
import React from "react"
import { Paper, TextField } from "@material-ui/core"
class SearchBar extends React.Component {
state = {
searchTerm: ""
}
handleChange = event => this.setState({ searchTerm: event.target.value })
handleSubmit = event => {
const { searchTerm } = this.state
const { onFormSubmit } = this.props
onFormSubmit(searchTerm)
event.preventDefault()
}
render() {
return (
<Paper elevation={6} style={{ padding: "25px" }}>
<form onSubmit={this.handleSubmit}>
<TextField
fullWidth
label="Search..."
onChange={this.handleChange}
></TextField>
</form>
</Paper>
)
}
}
export default SearchBar
So now the SearchBar component will run the method we inject into it and pass
it the searchTerm
.
We’ve also updated the handleSubmit
function so that it receives the event
and makes a call to event.preventDefault()
to stop the form submit from
refreshing the page.
Next we’re going to define our handleSubmit
method that we’re passing into
the SearchBar component within our App.js
.
As you can see we’re using the async
keyword before our function, and the
await
keyword before the call to the YouTube API call.
This is a new feature of ES2017 that makes it possible to make asynchronous calls using a standard synchronous functional definition instead of having to rely on promise chain.
// src/App.js
import React from "react"
import { Grid } from "@material-ui/core"
import { SearchBar, VideoDetail } from "./components"
import youtube from "./api/youtube"
class App extends React.Component {
handleSubmit = async searchTerm => {
const response = await youtube.get("search", {
params: {
q: searchTerm
}
})
console.log(response)
}
render() {
return (
<Grid justify="center" container spacing={10}>
<Grid item xs={12}>
<Grid container spacing={10}>
<Grid item xs={12}>
<SearchBar onFormSubmit={this.handleSubmit} />
</Grid>
<Grid item xs={8}>
<VideoDetail />
</Grid>
<Grid item xs={4}>
{/* VIDEO LIST */}
</Grid>
</Grid>
</Grid>
</Grid>
)
}
}
export default App
Fixing Axios Call
It turns out that this doesn’t work, we end up getting an HTTP 400 error from the YouTube API because Axios is not passing our default parameters.
First let’s cut those params
from our youtube.js file.
// src/api/youtube.js
import axios from "axios"
export default axios.create({
baseURL: "https://www.googleapis.com/youtube/v3"
})
And then paste them into the call from src/App.js
.
handleSubmit = async searchTerm => {
const response = await youtube.get("search", {
params: {
part: "snippet",
maxResults: 5,
key: "[Api Key]",
q: searchTerm
}
})
console.log(response)
}
If you look at the console you’ll see that the ‘data’ object in the API response contains the ‘items’ returned by the search. We can narrow down our console log statement to this.
handleSubmit = async searchTerm => {
const response = await youtube.get("search", {
params: {
part: "snippet",
maxResults: 5,
key: "[Api Key]",
q: searchTerm
}
})
console.log(response.data.items)
}
Now we have all the data we need to create our YouTube video list.
Displaying Search Results
Adding results to state
Within our App.js, we can now establish the definition of the default state
within our App class, and we can also use this.setState()
within our
handleSubmit
function so that it sets the ‘videos’ to equal the YouTube
search results we obtained, and it sets the ‘selectedVideo’ to the first video
in the search results collection.
// src/App.js
// ...
class App extends React.Component {
state = {
videos: [],
selectedVideo: null
}
handleSubmit = async searchTerm => {
const response = await youtube.get("search", {
params: {
part: "snippet",
maxResults: 5,
key: "[App Key]",
q: searchTerm
}
})
this.setState({
videos: response.data.items,
selectedVideo: response.data.items[0]
})
}
// render (){ ... }
}
// ...
Populating Video Detail
Now we can pass the selectedVideo
information to the VideoDetail
component.
// src/App.js
// ...
class App extends React.Component {
// state = ...
// handleSubmit = ...
render() {
const { selectedVideo } = this.state
return (
<Grid justify="center" container spacing={10}>
<Grid item xs={12}>
<Grid container spacing={10}>
<Grid item xs={12}>
<SearchBar onFormSubmit={this.handleSubmit} />
</Grid>
<Grid item xs={8}>
<VideoDetail video={selectedVideo} />
</Grid>
<Grid item xs={4}>
{/* VIDEO LIST */}
</Grid>
</Grid>
</Grid>
</Grid>
)
}
}
// ...
Now we can open the VideoDetail
component and build it out to use the object
we’re passing via the props.
First off we added an import of Paper
and Typography
from the Material-UI
library. With a function based components the props are passed as the first
argument to the function, so we’re using variable destructuring to bring in
only the ‘video’ property from the props passed in.
We’re using a React.Fragment
wrapper to wrap the two Paper component.
// src/components/VideoDetail.js
import React from "react"
import { Paper, Typography } from "@material-ui/core"
const VideoDetail = ({ video }) => {
return (
<React.Fragment>
<Paper elevation={6} style={{ height: "70%" }}></Paper>
<Paper elevation={6} style={{ padding: "15px" }}></Paper>
</React.Fragment>
)
}
export default VideoDetail
Within the first Paper element, we’re going to place an iframe that will display the video.
Note that the videoSrc
constant we define uses backticks around the string
so that the interpolation of the videoId
is supported.
// src/components/VideoDetail.js
import React from "react"
import { Paper, Typography } from "@material-ui/core"
const VideoDetail = ({ video }) => {
if (!video) return <div>Loading...</div>
const videoSrc = `https://www.youtube.com/embed/${video.id.videoId}`
return (
<React.Fragment>
<Paper elevation={6} style={{ height: "70%" }}>
<iframe
frameBorder="0"
height="100%"
width="100%"
title="Video Player"
src={videoSrc}
/>
</Paper>
<Paper elevation={6} style={{ padding: "15px" }}></Paper>
</React.Fragment>
)
}
export default VideoDetail
Now if you go check the app by doing a search, the video will load.
Video Text
Below the video we want to display the information about the video. The Typography component can support paragraphs, headers, etc.
// src/components/VideoDetail.js
import React from "react"
import { Paper, Typography } from "@material-ui/core"
const VideoDetail = ({ video }) => {
if (!video) return <div>Loading...</div>
const videoSrc = `https://www.youtube.com/embed/${video.id.videoId}`
return (
<React.Fragment>
<Paper elevation={6} style={{ height: "70%" }}>
<iframe
frameBorder="0"
height="100%"
width="100%"
title="Video Player"
src={videoSrc}
/>
</Paper>
<Paper elevation={6} style={{ padding: "15px" }}>
<Typography variant="h4">
{video.snippet.title} - {video.snippet.channelTitle}
</Typography>
<Typography variant="subtitle1">
{video.snippet.channelTitle}
</Typography>
<Typography variant="subtitle2">{video.snippet.description}</Typography>
</Paper>
</React.Fragment>
)
}
export default VideoDetail
There we have our video detail component, let’s focus on the video list.
Video List
Let’s start by creating a new component to render the video items in the list,
which we will call VideoItem
.
// src/components/VideoItem.js
import React from "react"
import { Grid, Paper, Typography } from "@material-ui/core"
const VideoItem = () => {
return <h1>Video Item</h1>
}
export default VideoItem
// src/components/VideoList.js
import React from "react"
import { Grid } from "@material-ui/core"
const VideoList = () => {
return <h1>VideoList</h1>
}
export default VideoList
// src/components/index.js
export { default as SearchBar } from "./SearchBar"
export { default as VideoDetail } from "./VideoDetail"
export { default as VideoList } from "./VideoList"
After establishing these, we’ll a the VideoList
component into the import
statement within App.js
, and update the remaining empty Grid item so that it
contains <VideoList />>
.
// src/App.js
import React from "react"
import { Grid } from "@material-ui/core"
import { SearchBar, VideoDetail, VideoList } from "./components"
import youtube from "./api/youtube"
class App extends React.Component {
// ...
render() {
const { selectedVideo } = this.state
return (
<Grid justify="center" container spacing={10}>
<Grid item xs={12}>
<Grid container spacing={10}>
<Grid item xs={12}>
<SearchBar onFormSubmit={this.handleSubmit} />
</Grid>
<Grid item xs={8}>
<VideoDetail video={selectedVideo} />
</Grid>
<Grid item xs={4}>
<VideoList />>
</Grid>
</Grid>
</Grid>
</Grid>
)
}
}
Now our VideoList component is rendering.
Iterating Over Items
Within our VideoList we need to iterate over the list of items obtained from the YouTube API and render a separate VideoItem for each one.
First let’s pass our list of videos to the VideoList component via the props.
We’ll do this by destructuring this.state
within the render()
function
so that both selectedVideo
and videos
are both present. We’ll then
pass videos
to the VideoList
component.
// src/App.js
// ...
class App extends React.Component {
// ...
render() {
const { selectedVideo, videos } = this.state
return (
<Grid justify="center" container spacing={10}>
<Grid item xs={12}>
<Grid container spacing={10}>
<Grid item xs={12}>
<SearchBar onFormSubmit={this.handleSubmit} />
</Grid>
<Grid item xs={8}>
<VideoDetail video={selectedVideo} />
</Grid>
<Grid item xs={4}>
<VideoList videos={videos} />
</Grid>
</Grid>
</Grid>
</Grid>
)
}
}
Now within the VideoList
, we’ll destucture the videos
property from props
,
and then use the Array.map()
method to generate an array of VideoItem
components that represent each item. This will require that we also import
our VideoItem
component within VideoList.js
.
// src/components/VideoList.js
import React from "react"
import { Grid } from "@material-ui/core"
import VideoItem from "./VideoItem"
const VideoList = ({ videos }) => {
const listOfVideos = videos.map(video => <VideoItem />)
return listOfVideos
}
export default VideoList
You go to the browser and test your app now, a search should result in ‘Video Item’ shown 5 times on the right side of the screen.
Do remember though that when you’re mapping over a collection you need to
provide a unique key for each item. The Array.map
function will provide an
index integer as the second argument so we can simply use that.
We’ve also added the video itself as another prop.
const listOfVideos = videos.map((video, id) => (
<VideoItem key={id} video={video} />
))
Expanding the Video Items
Within the VideoItem
component,
// src/components/VideoItem.js
import React from "react"
import { Grid, Paper, Typography } from "@material-ui/core"
const VideoItem = ({ video }) => {
return (
<Grid item xs={12}>
<Paper style={{ display: "flex", alignItems: "center" }}>
<img
style={{ marginRight: "20px" }}
alt="thumbnail"
src={video.snippet.thumbnails.medium.url}
/>
<Typography variant="subtitle1">
<b>{video.snippet.title}</b>
</Typography>
</Paper>
</Grid>
)
}
export default VideoItem
This displays the medium thumbnail images for each item, with the title shown to the right of each thumbnail.
Further Styling
Let’s return the VideoList within a Grid container.
// src/components/VideoList.js
import React from "react"
import { Grid } from "@material-ui/core"
import VideoItem from "./VideoItem"
const VideoList = ({ videos }) => {
const listOfVideos = videos.map((video, id) => (
<VideoItem key={id} video={video} />
))
return (
<Grid container spacing={10}>
{listOfVideos}
</Grid>
)
}
export default VideoList
This adds a bit of spacing to our items displayed in the list.
Linking Items to the Selected Video
Now we’re going to make it so that when someone selects a video from the list
it loads in the selectedVideo
state in our App
component.
This requires that we simply pass a function to the VideoItem
component named
as the onVideoSelect
prop, and then make that a prop that VideoList
receives
as a prop from App.js
.
// src/components/VideoList.js
import React from "react"
import { Grid } from "@material-ui/core"
import VideoItem from "./VideoItem"
const VideoList = ({ videos, onVideoSelect }) => {
const listOfVideos = videos.map((video, id) => (
<VideoItem onVideoSelect={onVideoSelect} key={id} video={video} />
))
return (
<Grid container spacing={10}>
{listOfVideos}
</Grid>
)
}
export default VideoList
Now let’s define this function in App.js
.
// src/App.js
// ...
class App extends React.Component {
// ...
onVideoSelect = video => {
this.setState({ selectedVideo: video })
}
render() {
const { selectedVideo, videos } = this.state
return (
<Grid justify="center" container spacing={10}>
<Grid item xs={12}>
<Grid container spacing={10}>
<Grid item xs={12}>
<SearchBar onFormSubmit={this.handleSubmit} />
</Grid>
<Grid item xs={8}>
<VideoDetail video={selectedVideo} />
</Grid>
<Grid item xs={4}>
<VideoList videos={videos} onVideoSelect={this.onVideoSelect} />
</Grid>
</Grid>
</Grid>
</Grid>
)
}
}
Now lastly we need to bind this prop to the onClick event. You’ll see that
we’ve added onVideoSelect
as a property being destructured from the props
argument passed to our component. We’re also bound our click event to the
Paper
element that displays the thumbnail.
// src/components/VideoItem.js
import React from "react"
import { Grid, Paper, Typography } from "@material-ui/core"
const VideoItem = ({ video, onVideoSelect }) => {
return (
<Grid item xs={12}>
<Paper
style={{ display: "flex", alignItems: "center" }}
onClick={() => onVideoSelect(video)}
>
<img
style={{ marginRight: "20px" }}
alt="thumbnail"
src={video.snippet.thumbnails.medium.url}
/>
<Typography variant="subtitle1">
<b>{video.snippet.title}</b>
</Typography>
</Paper>
</Grid>
)
}
export default VideoItem
And now we’re done.