Introduction

This article is for anyone wanting to learn and advance in React, by creating a real project from scratch. Building this project will solidify your knowledge of all the main concepts of React. This project will include React components, project structure, lifecycle methods, state management, passing props from parent to child components, API requests and much more.

This article is suitable for everyone who is just starting with React and for those that already know quite a bit but want to improve their knowledge.

If the app stops working for you at a certain step, it is possible that a small typo occurred. Feel free to refer to this GitHub repository and copy the code, so you can continue working on the project immediately without trying to solve errors for hours. Also, feel free to give it a star! ⭐ :)

Just a note

Along with writing this article, I have also created a YouTube video!

I cannot stress enough the fact that the video has detailed explanations of every line of code. I encourage you to first watch the video and follow along / code while watching. The video goes into much more depth and has more comprehensive explanations than this article does. You can then use the concise code blocks in this article as a reference.

Video also touches the topics of best practices in React (destructuring, imports/exports, class based vs functional components...) and is more beginner friendly since important things will be repeated through the video.

Here's a link to the full video: https://youtu.be/VPVzx1ZOVuw.

Project overview

You can check the functionality of the finished product at the beginning of the video.

Project Setup

To speed up the process of creating all the necessary directories and files, we're going to use command line. If you're using Visual Studio Code, there's a built in terminal which is quite convenient, but if you're not using VSC, you can use Command Prompt that comes with Windows or Terminal on Mac.

  1. Create a new directory
    mkdir youtube-clone-app && cd youtube-clone-app
  2. Create a new React App with
    create-react-app ./
    and then move into newly created directory and install dependencies
    npm i -S axios @material-ui/core
  3. Run the project with
    npm start
  4. Open another terminal window on the side so you can execute additional commands simultaneously while your app is running. (Plus icon on VSC).

Dive into coding

Before we start writing code, we're going to delete unnecessary boilerplate code create-react-app provided us with.

rm -rf src

Now we can create a new src directory by typing:

mkdir src

And we can add two of the most important files of the project:

 touch src/App.js src/index.js

Inside of the index.js file, we're going to initiate our React application:

import React from 'react';
import ReactDOM from 'react-dom';

import App from './App';

ReactDOM.render(<App />, document.querySelector('#root'));

Inside of App.js we're going to set up our initial project structure:

import React from 'react';

import { Grid } from '@material-ui/core';

class App extends React.Component {
	render() {
      return (
        <Grid style={{ justifyContent: 'center' }} container spacing={10}>
          <Grid item xs={11}>
            <Grid container spacing={10}>
              <Grid item xs={12}>
              	{/* This is where SearchBar component will go */}
              </Grid>
              <Grid item xs={8}>
              	{/* This is where VideoDetail component will go */}
              </Grid>
              <Grid item xs={4}>
              	{/* This is where VideoList component will go */}
              </Grid>
            </Grid>
          </Grid>
        </Grid>
      );
    }
}

export default App;

The next thing on the list is setting up our API call. You need to login to your Google account on https://console.developers.google.com. You can then search the API's for YouTube Data API v3, and get credentials for it. If you get stuck, feel free to reference to the video mentioned above.

Afterwards, we can create a new folder and a file inside of /src directory:

mkdir src/api
touch src/api/youtube.js

Inside of the newly created file we can import axios and initiate our axios instance:

import axios from 'axios';

export default axios.create({
    baseURL: 'https://www.googleapis.com/youtube/v3',
});

Now we can import that youtube instance into our App.js

Just add import youtube from './api/youtube'; somewhere at the top of your App.js file.

While we're importing stuff, let's also import all the components that will be necessary for the full application. But first, we need to create them:

mkdir src/components
Creates a directory
cd src/components
Moves into that directory
touch VideoDetail.js VideoList.js SearchBar.js index.js
Creates all the necessary files
cd ../../
Moves back into the root directory

Class based vs Functional components

React provides two different types of components - we'll call them functional and class based. Just below are some of the upsides and downsides of both, and a suggestion when to use each one.

A functional component is a regular pure JavaScript function that accepts props as its argument, and returns some JSX.

const Hello = ({name}) => <div>Hello {name}</div>

Here is the same component written as a Class based one:

class Hello extends Component{
   render(){
      return <div>Hello {this.props.name}</div>
   }
}

Functional components are also called dumb components. They do not need to manage the state, execute some logic or worry about lifecycle methods. They are simply presentational and return JSX.

On the other hand, Class based components have access to the state object, lifecycle methods and more. They can became more complicated and allow you to do more stuff with them.

Good rule of thumb is to use functional components whenever you can. Any component can start as a dumb one. But as soon as you notice that you might need state in there, just transform it to smart Class based component.

In our application, App.js and SearchBar.js are going to be smart Class based components. They are going to manage the state and execute some logic. All other components will be functional components.

Let's create a skeleton for our three components so we can import them into our App.js and visualize them in the browser.

SearchBar.js

import React from 'react';

class SearchBar extends React.Component {
    render() {
      return (<h1>SearchBar Component</h1>)
    }
}

export default SearchBar;

VideoDetail.js

import React from 'react';

const VideoDetail = () => {
  return (<h1>VideoDetail Component</h1>)
}

export default VideoDetail;

VideoList.js

import React from 'react';

const VideoList = () => {
  return (<h1>VideoList Component</h1>)
}

export default VideoList;

And now, inside of our index.js that's inside of src directory, we can export them all from the components:

index.js

export { default as SearchBar } from './SearchBar';
export { default as VideoDetail } from './VideoDetail';
export { default as VideoList } from './VideoList';

By using this way of exporting, we can import all the components in one line in App.js:

import { SearchBar, VideoList, VideoDetail } from './components';

And now we can put them where our comments were before:

import React from 'react';

import { Grid } from '@material-ui/core';

import { SearchBar, VideoList, VideoDetail } from './components';

import youtube from './api/youtube';

class App extends React.Component {
	render() {
      return (
        <Grid style={{ justifyContent: 'center' }} container spacing={10}>
          <Grid item xs={11}>
            <Grid container spacing={10}>
              <Grid item xs={12}>
              	<SearchBar />
              </Grid>
              <Grid item xs={8}>
                <VideoDetail />
              </Grid>
              <Grid item xs={4}>
                <VideoList />
              </Grid>
            </Grid>
          </Grid>
        </Grid>
      );
    }
}

export default App;

If everything is done correctly, this should be displaying in the browser:

Setting up the SearchBar component

SearchBar is one of the smart components, inside of here we'll need to manage the state of the input field. So let's set it up.

First, we need to import some of the elements from MaterialUI:

import { Paper, TextField } from '@material-ui/core';

Secondly, we're going to initialize our state and set up two methods, handleChange and handleSubmit:

	state = { searchTerm: '' };

	handleChange = event => this.setState({ searchTerm: event.target.value });

    handleSubmit = (event) => {
      const { searchTerm } = this.state;
      const { onFormSubmit } = this.props;

      onFormSubmit(searchTerm);

      event.preventDefault();
    }

The onFormSubmit function coming from the this.props is till not available to us. We're going to create it in the App.js file.

And now we create the looks of our search bar and use these methods:

render() {
      const { searchTerm } = this.state;

      return (
        <Paper elevation={6} style={{ padding: '25px' }}>
          <form onSubmit={this.handleSubmit}>
            <TextField fullWidth label="Search..." value={searchTerm} onChange={this.handleChange} />
          </form>
        </Paper>
      );
    }

We're using the handleSubmit on the onSubmit listener on the form element. And handle change on the onChange listener on TextField element.

This is the code for the full component:

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() {
      const { searchTerm } = this.state;

      return (
        <Paper elevation={6} style={{ padding: '25px' }}>
          <form onSubmit={this.handleSubmit}>
            <TextField fullWidth label="Search..." value={searchTerm} onChange={this.handleChange} />
          </form>
        </Paper>
      );
    }
}

export default SearchBar;

Now we can create our initial state and handleSubmit function inside of the App.js. This is where the fetching logic will happen.

 state = {
    videos: [],
    selectedVideo: null,
  }
  
handleSubmit = async (searchTerm) => {
    const response = await youtube.get('search', {
      params: {
        part: 'snippet',
        maxResults: 5,
        key: '[YOUR_API_KEY]',
        q: searchTerm,
      }
    });

    this.setState({ videos: response.data.items, selectedVideo: response.data.items[0] });
  }

In here, we can asynchronous function, that uses the axios instance we previously created and is making a get request. We need to pass some parameters so that API works correctly. Snippet means that we want additional data about each video, max results is self explanatory, key is the API key that you need to paste yourself (the link to get it is somewhere on top of the article). And the q (means query) will be equal to our searchTerm that we're passing from our SearchBar component. Afterwards, we're setting state of our videos to newly fetched data, and the state of our selectedVideo to the first item in the videos array.

If you have any issues with the fetching the data from the API, refer to this video - .

Here is how the data that we're fetching looks like:

Everything that we need is inside of response.data.items and then under each item, we have a snippet, which has data about channelId, title, description, etc.

Just at the top of our render method, destructure the state:

const { selectedVideo, videos } = this.state;

Now we can pass that handleSubmit as onFormSubmit to our SearchBar:

<SearchBar onFormSubmit={this.handleSubmit} />

While we're here, also pass video to our VideoDetail:

<VideoDetail video={selectedVideo}/>

Setting up the VideoDetail component

Finally, we get to see our data!

First, we need to import some of the elements from MaterialUI:

import { Paper, TextField } from '@material-ui/core';

Secondly, we can now get the video property that we passed, using destructuring, that will look like this:

const VideoDetail = ({ video }) => {
  return (
  	...
  )
}

Inside of our video detail, we're first going to check if there is a video, if there's not, we're going to display a loading message:

if(!video) return <div>Loading...</div>

Now we can structure the src of our video. We're going to do that using the videoId coming from our API call:

const videoSrc = `https://www.youtube.com/embed/${video.id.videoId}`

Finally, our return statement is going to look like this:

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>
  )

In here, we're using all the meaningful data to display some info about the video, and we're using the iframe to play it in the browser.

This is the code for completed  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;

Setting up the VideoList and VideoItem components

First, pass videos from our App.js to our VideoList.

 <VideoList videos={videos} />

VideoList component will be a parent component that will simply loop the other component we're going to create. So first, let's create VideoItem.js:

touch src/components/VideoItem.js

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', cursor: 'pointer' }}>
        <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;

In here, we're taking some additional meaningful data, and now we're going to use this component to create 5 different videos on the side.

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;

The data is properly displaying right now, but video items are not clickable. Remember the selectedVideo state we have in the App.js component? We're going to use it now.

Inside of the App.js, create another method:

onVideoSelect = (video) => {
   this.setState({ selectedVideo: video });
 }

Now pass that method to our VideoList component:

<VideoList videos={videos} onVideoSelect={this.onVideoSelect} />

And now, inside of our VideoList component, pass that method one more time, like this:

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;

We're getting it as props, and passing it to VideoItem component. Now we can use it there, take a look at the onClick listener on the Paper component:

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', cursor: 'pointer' }} 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;

That’s it!

You made it all the way until the end! If you'd want me to cover another project in the next article/video, feel free to leave a comment on a YouTube video stating your idea.

Feel free to ask any questions and leave feedback or critique.

Most helpful would be the support on YouTube since I recently created a channel! Click here, there is a lot of interesting stuff coming soon! :)

If you're wondering how to put this project on GitHub so you can display it on your portfolio, here's the video I did on: Git Commands.