Build a YouTube Queue with Next.js: Part 1

Khalea B.

Khalea B. / June 15, 2021

7 min read

3D rendered YouTube play button. Photo by Alexander Shatov on Unsplash.

In recent months, I've become fascinated with shared, online spaces. Applications like Clubhouse, Discord, and lesser-known platforms such as Beatsense have been integral to maintaining a sense of community throughout the pandemic.

In this tutorial, we'll be creating a shared space where you can add & watch YouTube videos to a queue, and chat in real time with other users. In part 1, we'll focus on the videos.

Prerequisites

  • Familiarity with React
  • Familiarity with Next.js
  • Node.js 10.13 or later
  • create-next-app (npm i create-next-app)

Getting Started

First you'll want to initialize a new Next.js project and set up TailwindCSS. In the command line, navigate to the location of your choice and run:

npx create-next-app -e with-tailwindcss youtube-social

To install Tailwind and its dependencies, navigate to the youtube-social folder in the CLI and run:

npm install -D tailwindcss@latest postcss@latest autoprefixer@latest

Then generate the default configuration files tailwind.config.js and postcss.config.js:

npx tailwindcss init -full -p

To start the development server run:

npm run dev

When you go to http://localhost:3000, you should see the default "Welcome to Next.js" page.

Player Component

We'll be using the nifty react-player/youtube library created by Pete Cook to easily handle video events related to the queue of YouTube videos.

First, use the CLI to install react-player in your project:

npm install react-player

Create a new folder in the root level of your project called components. We'll store independent 'components' of the app in this folder, making them reusable and easy to modify.

Add a new file called player.js into the components folder. This will contain the YouTube embed.

In player.js, import react-player/youtube like so:

import ReactPlayer from 'react-player/youtube'

Next, create a functional component called Player that accepts props. The props we'll pass in are a YouTube video ID and a callback function to be executed when a video ends.

Within it, declare a variable called videoURL that holds the base YouTube video URL and the video ID, and return a <div> containing a <ReactPlayer>:

export default function Player(props) {
    const videoURL = "https://www.youtube.com/watch?v=" + props.videoId
  return(
    <div>
      <ReactPlayer />
    </div>
  )
}

The ReactPlayer accepts a variety of props that can help us manage events related to the player. We will use:

url: (String) The url of the YouTube video playing: (Boolean) Video plays automatically onEnded: (Callback) Function that executes when the video ends config: (JSON) Options for the YouTube embed

export default function Player(props) {
    const videoURL = "https://www.youtube.com/watch?v=" + props.videoId
    return(
        <div>
            <ReactPlayer
                url={videoURL}
                playing={true}
                onEnded={props.onEnd}
                config={{
                    youtube: {
                        playerVars: {
                            autoplay: 1,
                            controls: 1
                        }
                    }
                }}
            />
        </div>
  )
}

Import the Player component into index.js. Remove all of the boilerplate code within the outer <div>s. Place the Player under the <main> section of the page. As a placeholder, we'll pass in a hard-coded videoId for a Kurzgesagt video:

import Head from 'next/head'
import Player from '../components/player'

export default function Home() {
  return (
    <div className="flex flex-col items-center justify-center min-h-screen py-2">
      <Head>
        <title>YouTube Social</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main>
        <Player videoId={"JXeJANDKwDc"}/>
      </main>
    </div>
  )
}

Save your work in the editor, and the page should reload and start playing the video.

Queue Component

Now, let's set up the queue component. We want to take JSON formatted video data from the YouTube API and extract the video IDs, titles, and channel titles and display them as part of the queue.

We'll start with some hard coded data that mimics the YouTube API response. Create a folder called data in the root of your project. Then add a file called test-queue.json into it, and copy the following test data:

{
    "items": [
        {
            "id": {
                "videoId": "3qqzz9a8pMQ"
            },
            "snippet": {
                "title": "Grimes - Aeon / Kill V. Maim / Di-Li-Do (Acapella) [Mixed]",
                "channelTitle": "sef",
                "thumbnails": {
                    "default": {
                        "url": "https://i.ytimg.com/vi/3qqzz9a8pMQ/default.jpg",
                        "width": 120,
                        "height": 90
                    }
                }
            }
        },
        {
            "id": {
                "videoId": "HlLx7oE7q3I"
            },
            "snippet": {
                "title": "Hozier - Dinner & Diatribes (Official Video)",
                "channelTitle": "HozierVEVO",
                "thumbnails": {
                    "default": {
                        "url": "https://i.ytimg.com/vi/Iq5gesj6kmw/default.jpg",
                        "width": 120,
                        "height": 90
                    }
                }
            }
        },
        {
            "id": {
                "videoId": "M9teCJVTr_s"
            },
            "snippet": {
                "title": "Yves Tumor - Licking An Orchid (ft. James K)",
                "channelTitle": "Yves Tumor",
                "thumbnails": {
                    "default": {
                        "url": "https://i.ytimg.com/vi/M9teCJVTr_s/default.jpg",
                        "width": 120,
                        "height": 90
                    }
                }
            }
        }
,
        {
            "id": {
                "videoId": "p2Rro6TQgpU"
            },
            "snippet": {
                "title": "FKA twigs - home with you",
                "channelTitle": "FKA twigs",
                "thumbnails": {
                    "default": {
                        "url": "https://i.ytimg.com/vi/p2Rro6TQgpU/default.jpg",
                        "width": 120,
                        "height": 90
                    }
                }
            }
        },
        {
            "id": {
                "videoId": "286jXjwdst0"
            },
            "snippet": {
                "title": "Apashe ft. Instasamka - Uebok (Gotta Run) [Official Video]",
                "channelTitle": "Kannibalen Records",
                "thumbnails": {
                    "default": {
                        "url": "https://i.ytimg.com/vi/gM4xZy39kNE/default.jpg",
                        "width": 120,
                        "height": 90
                    }
                }
            }
        },
        {
            "id": {
                "videoId": "xqYFU1_SiBo"
            },
            "snippet": {
                "title": "Untitled (2012 Remaster)",
                "channelTitle": "Interpol",
                "thumbnails": {
                    "default": {
                        "url": "https://i.ytimg.com/vi/xqYFU1_SiBo/default.jpg",
                        "width": 120,
                        "height": 90
                    }
                }
            }
        }
    ]
}

Add a new component called queue.js to the components folder. The queue component should accept a prop called data containing the video data. The data will be mapped to unique rows like so:

export default function Queue(props) {

        return (
            <div className="max-w-sm max-h-96 px-16 overflow-y-scroll">
                <h3 className="font-bold">Queue</h3>
                {

                    props.data.map((item, index) => {
                        return (
                            <div key={index} className="flex flex-row items-center w-60">
                                <h1 className="text-lg">{index + 1}</h1>

                                <div className="text-sm">
                                    <h3 className="font-bold">{item.snippet.title}</h3>
                                    <p>{item.snippet.channelTitle}</p>
                                </div>
                            </div>
                        )
                    })
                }
            </div>
        )
}

We'll want to use the useState hook in order to track and manage the state of the video data. useState will help us dynamically update the Queue component once videos are removed from the video data list.

In the index.js file, import the TestQueue, the Queue component, and useState:

import TestQueue from '../data/test-queue.json'
import Queue from '../components/queue'
import { useState } from React

In the Home component, add the useState hook with variables called videoData and updateVideoData. Set the initial state of videoData to TestQueue.items:

const [videoData, updateVideoData] = useState(TestQueue.items)

We'll place the Player and Queue components side by side using Flexbox with Tailwind. We'll also pass the videoData to the Queue component as a prop. This will keep the Queue component updated as videoData changes.

<main>
        <div className="flex flex-row">
            <Player videoId={"JXeJANDKwDc"}/>
            <Queue data={videoData}/>
        </div>
</main>

Player events

You may notice that the first video plays to completion, but the next video in the queue does not automatically start once it ends. To enable this behavior, we need to use the onEnd prop of the Player to pass in a callback that initiates a shift in the queue, and the current video.

Let's start by adding a useState hook for the currentVideoId in our index.js file. Set the default value to the first video in the queue:

const [currentVideoId, updateCurrentVideoId] = useState(videoData[0].id.videoId)

Now pass the currentVideoId down to the Player component:

<Player videoId={currentVideoId}/>

Next up is the onEnd callback. We need to create a function that updates the queue and the current video, then pass it to the Player via the onEnd prop:

const playNext = () => {
    videoData.shift()
    if (videoData.length > 0) {
      updateCurrentVideoId(videoData[0].id.videoId)
    }
  }
<Player videoId={currentVideoId} onEnd={playNext}/>

Conclusion

Voila, you now have a basic YouTube playlist application! In the next part, I'll show you how to use the YouTube Data API to implement search. This code is also available on Github.