ReactからGoogleapis の Youtube Data APIを呼び出す方法 ~Search APIを呼び出して動画の検索ページと再生ページを作成する~

React から外部のAPIを呼び出すサンプルを作成してみました。

外部APIの題材としてYoutubeのData APIのSearchを利用し、動画の検索ページと再生ページも作成していきます。

システム構成

今回はAPI部分を中心に説明しますが、デプロイ先はFirebaseのHostingサービス、CI/CDはGitHub Acrionsを利用しています。

GitHub Actionsに興味のある方は以下の記事で利用方法を紹介しています。

“https://snowsystem.net/git/github-actions-react-firebase/

  

システム構成図

Youtube Data APIを利用できるようにする

Youtube Data APIはGoogleのサービスですので、利用するためにはGoogle Developersコンソールでプロジェクトの作成やAPIの有効化といった手順が必要になります。

Google Developers Console でプロジェクトを作成する

以下のページにアクセスするとGoogle Developersのコンソール画面が表示されます。

Google Cloud Platform
Google Cloud Platform lets you build, deploy, and scale applications, websites, and services on the same infrastructure ...

 

コンソール画面が表示されたら「新しいプロジェクト」から作成します。

既にFirebaseやGCPを利用されている場合は、対象のプロジェクトを選択しましょう。

Youtube APIを有効化する

「APIとサービス」の「ダッシュボード」で「+APIとサービスの有効化」をクリック

 

Youtube Data API v3を選択

一覧表示されている中から「Youtube Data API v3」を選択します。

探しづらい場合は、検索ボックスで「youtube」と検索すると絞り込むことができます。

 

「有効化」ボタンをクリック

「Youtube Data API v3」を開き「有効化」ボタンをクリックするとそのプロジェクト内で利用可能になります。

認証情報を作成する

「APIとサービス」の「認証情報」で「認証情報を作成」=>「APIキー」を選択する

APIの制限をする

「APIの制限」の「キーを制限」から「Youtube Data API v3」を選択する

Youtube API呼び出しコンポーネントを実装する

Youtube Data APIはREST形式で呼び出すことで利用できます。

今回は「axios」を利用して指定した検索キーワードにヒットした動画の情報を取得する機能を作成します。

ソースコード

import axios, { AxiosRequestConfig } from 'axios';

export const YoutubeSearch: any = async (keyword: string) => {
  try {
    // console.log(keyword);

    const config: AxiosRequestConfig = {
      url: 'https://www.googleapis.com/youtube/v3/search',
      method: 'GET',
      headers: {
        'Content-Type': 'application/json; charset=utf-8',
      },
      params: {
        part: 'snippet',
        q: keyword,
        maxResults: 50,
        key: process.env.REACT_APP_API_KEY  // 取得したAPIキーを設定
      }
    }
    const res = await axios(config);

    // console.log(res.data.items);

    return res.data.items;

  } catch (error) {
    throw (error);
  }
};

パラメータ

設定したパラメータは以下の通りですが、他のパラメータを確認したい場合はここに載っています。

項目設定値
urlhttps://www.googleapis.com/youtube/v3/search
methodGET
headersContent-Typeがjsonであることを指定
partsnippet
q検索したい文字列
maxResults取得する動画リストの数(0~50)
何も指定しない場合は5になります。
key取得したAPIキーの文字列

 

async/await

WebAPIを利用する場合は、非同期処理となりますので「コールバック関数」「Promise」「async/await」の何れかの構文を利用して実装します。

上記のサンプルではES2017以降で対応したasync/awaitで実装しています。

async/awaitは直感的でわかりやすいですよね。

検索ボックスコンポーネントを作成する

画面イメージ

イメージとしては以下のような検索ボックスに入力されたキーワードを先程作成したYoutubeSearchのWebAPIのコンポーネントを呼び出します。

ソースコード

import styled from 'styled-components';
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { searchOperation } from '../../reducks/youtube/operations'

import { SearchIcon } from '../svg/SearchIcon';

export const YoutubeSearchBox: React.FC = () => {

  const dispatch = useDispatch();

  const [keyword, setKeyword] = useState<string>('');

  // Searchボタンクリック
  const handleOnSubmitYoutubeSearch = (event: React.FormEvent<HTMLFormElement>) => {

    event.preventDefault();
    event.stopPropagation();

    if (checkValidate()) {
      dispatch(searchOperation(keyword));
    }
  };

  const checkValidate = () => {
    let ret = true;

    if (!keyword) {
      ret = false;
    }
    return ret;
  }

  return (
    <SearchForm noValidate onSubmit={handleOnSubmitYoutubeSearch}>
      <SearchIcon />
      <SearchInput
        type="text"
        placeholder="Search…"
        value={keyword}
        onChange={(event) => setKeyword(event.target.value)}
        required
      />
    </SearchForm>
  )
}

const SearchForm = styled.form`
  display: flex;
  justify-content: center;
  align-items: center;
  width: min(100%, 576px);
  height: 36px;
  background-color: #444;
  border-radius: 4px;
  padding: 0 12px;

  &:hover{
    background-color: #555;
  }
`

const SearchInput = styled.input`
  background-color: inherit;
  color: #fff;
  font: 500 16px;
  width: calc(100% - 40px);
  margin: 0 0 0 10px;

  &::placeholder {
    color: #bbb;
  }
`

handleOnSubmitYoutubeSearch

検索ボックス内でEnterキーが押下された際にhandleOnSubmitYoutubeSearchを呼び出しています。

直接YoutubeSearchを呼び出しているわけではなくreduxでラッピングしたsearchOperationを呼び出しその中でYoutubeSearchを呼び出しています。

 

styled-components

コンポーネントのデザイン(CSS)にはstyled-componentsを利用しています。

styled-componentsはJavascriptのテンプレートリテラル構文を用いた形式で記述することができ、Javascriptでコーディングする感覚でCSSを記載できるのが特徴です。

styled-componentsが通常のタグに対していCSSを設定することもできますが、material-uiのコンポーネントに対してもCSSを設定することもできます。

興味のある方は以下の記事で説明しています。

コンポーネントとCSSをセットで管理できるため、今はstyled-componentsが使いやすくよく利用しています。

検索結果の表示ページを作成する

画面イメージ

検索キーワードに一致するYoutube動画をカードタイプのコンポーネントで表示するようにします。

以下は、検索キーワードとして「ノートパソコン」と入力した場合です。

ソースコード

YoutubeList

取得したvideoIdやurl、タイトル、公開日などの動画情報をカードタイプのコンポーネントに連携する親コンポーネントです。

import styled from 'styled-components';
import React, { useState, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { typYoutube } from '../../reducks/youtube/types';
import { getItems } from '../../reducks/youtube/selector';
import { typFavorites } from '../../reducks/favorites/types';
import { getFavorites } from '../../reducks/favorites/selector';
import { YoutubeCard } from '../../components/youtube/YoutubeCard'

export const YoutubeList: React.FC = () => {

  const youtubeSelector = useSelector<typYoutube, typYoutube>((state) => state);
  const youtubeItems = getItems(youtubeSelector);

  const favoriteSelector = useSelector<typFavorites, typFavorites>((state) => state);
  const favoriteItems = getFavorites(favoriteSelector);

  const [youtbeFavoriteItems, setyoutbeFavoriteItems] = useState<any[]>([]);

  useEffect(() => {
    setyoutbeFavoriteItems([...youtubeItems].map(youtubeItem => {
      const favoriteItem = favoriteItems.find(item => item.videoId === youtubeItem.id.videoId);

      return {
        videoId: youtubeItem.id.videoId,
        url: youtubeItem.snippet.thumbnails.medium.url,
        title: youtubeItem.snippet.title,
        channelTitle: youtubeItem.snippet.channelTitle,
        publishedAt: youtubeItem.snippet.publishedAt,
        isFavorte: !!favoriteItem,
      }
    }))
  }, [youtubeItems, favoriteItems]);

  return (
    <Container>
      {youtbeFavoriteItems.map((item) =>
        <YoutubeCard
          key={item.videoId}
          videoId={item.videoId}
          url={item.url}
          title={item.title}
          channelTitle={item.channelTitle}
          publishedAt={item.publishedAt}
          isFavorite={item.isFavorte}
        />
      )}
    </Container>
  )
}


const Container = styled.div`
  display: flex;
  justify-content: left;
  align-items: flex-start;
  flex-wrap: wrap;
  width: 100%;
`

YoutubeCard

YoutubeListから受け取った各種情報を元に上部に画像、下部にタイトルなどの情報を表示します。

import styled from 'styled-components';
import React from 'react'
import { useDispatch, useSelector } from 'react-redux';
import { addFavoritesOperation, deleteFavoritesOperation } from '../../reducks/favorites/operations'
import { playOperation } from '../../reducks/youtube/operations'
import { getUserId } from '../../reducks/users/selector'
import { typUsers } from '../../reducks/users/types'

import { useMediaQuery } from '../../components/use/useMediaQuery'

import { FavoriteFillIcon } from '../svg/FavoriteFillIcon';
import { FavoriteIcon } from '../svg/FavoriteIcon';

interface Props {
  videoId: string,
  url: string,
  title: string,
  channelTitle: string,
  publishedAt: string,
  isFavorite: boolean,
}

export const YoutubeCard: React.FC<Props> = ({ videoId, url, title, channelTitle, publishedAt, isFavorite }) => {

  const dispatch = useDispatch();

  const mq = useMediaQuery();

  const userSelector = useSelector<typUsers, typUsers>((state) => state);
  const userId = getUserId(userSelector);

  const handleOnClickAddFavorite = () => {
    const data = {
      videoId: videoId,
      url: url,
      title: title,
      channelTitle: channelTitle,
      publishedAt: publishedAt,
    }
    dispatch(addFavoritesOperation(userId, data));
  };

  const handleOnClickDelFavorite = () => {
    const data = {
      videoId: videoId,
      url: url,
      title: title,
      channelTitle: channelTitle,
      publishedAt: publishedAt,
    }
    dispatch(deleteFavoritesOperation(userId, data));
  };

  const handleOnClickPlay = () => {
    console.log(videoId);
    dispatch(playOperation(videoId));
  }

  return (
    <Container isPc={mq.isPc}>
      <Card>
        <YoutubeImage src={url} onClick={handleOnClickPlay} />
        <YoutubeTitle>
          {title}
        </YoutubeTitle>
        <ChannelTitle>
          {channelTitle}
        </ChannelTitle>
        <PublishedAt>
          {publishedAt.replace('T', ' ').replace('Z', '')}
        </PublishedAt>
        {!isFavorite &&
          <IconButton onClick={handleOnClickAddFavorite}>
            <FavoriteIcon />
          </IconButton>
        }
        {isFavorite &&
          <IconButton onClick={handleOnClickDelFavorite}>
            <FavoriteFillIcon />
          </IconButton>
        }
      </Card>
    </Container >
  )
}

const Container = styled.div<{ isPc: boolean }>`
  width: ${({ isPc }) => isPc ? 'max(250px, 25%)' : '100%'};
  padding: 10px;
`

const Card = styled.div`
  background-color: #fff;
  border-radius: 5px;
  box-shadow: 2px 2px 4px #555;
`


const YoutubeImage = styled.img`
  width: 100%;
  border-radius: 5px 5px 0 0;
  cursor: pointer;

  &::before {
    content: "";
    display: block;
    padding-top: 56.25%; /* 16:9 */
  }
`

const YoutubeTitle = styled.div`
  height: 50px;
  font: 500 16px;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  margin: 12px;
`

const ChannelTitle = styled.div`
  white-space: nowrap;
  text-overflow: ellipsis;
  overflow: hidden;
  color:#555;
  font: 300 12px;
  margin: 12px;
`

const PublishedAt = styled.div`
  white-space: nowrap;
  text-overflow: ellipsis;
  overflow: hidden;
  color:#555;
  font: 300 12px;
  margin: 12px;
`

const IconButton = styled.div`
  width: 48px;
  height: 48px;
  display: flex;
  justify-content: center;
  align-items: center;
  cursor: pointer;

  &:hover{
    color: #0cf;
    background-color: #eee;
    border-radius: 50%;
  }
`

お気に入り機能やCSSなどを作り込んでいるので、コードが長くなっていますが、今回のやりたいことであるクリックされた動画を再生する機能としてはシンプルです。

画像をクリックされた場合にYoutubeImageのonClick={handleOnClickPlay}でplayOperation(videoId)を呼び出しているだけです。

playOperationはreduxのコンポーネントとして実装していますが、機能としては再生用のページへの画面遷移です。

Youtube再生ページを作成する

検索結果の一覧画面から選択された動画を再生するページです。

Youtube動画再生ページ仕組みは簡単です。以下のようなURLをiFrameに指定するだけです。

- YouTube
YouTube でお気に入りの動画や音楽を楽しみ、オリジナルのコンテンツをアップロードして友だちや家族、世界中の人たちと共有しましょう。

画面イメージ

横幅いっぱいに表示するように設定することにします。

CSSでYoutubeの動画の縦横比である16:9を実現していきましょう。

ソースコード

import styled from 'styled-components';
import React from 'react'
import { useSelector } from 'react-redux';
import { typYoutube } from '../../reducks/youtube/types';
import { getPlayVideoId } from '../../reducks/youtube/selector';

export const YoutubePlayer: React.FC = () => {

  const youtubeSelector = useSelector<typYoutube, typYoutube>((state) => state);
  const youtubePlayVideoId = getPlayVideoId(youtubeSelector);

  return (
    <VideoContainer>
      <Video src={`https://www.youtube.com/embed/${youtubePlayVideoId}`} allowFullScreen />
    </VideoContainer>
  )
}


const VideoContainer = styled.div`
  width: 100%;
  padding-bottom: 56.25%;
  height: 0px;
  position: relative;
`

const Video = styled.iframe`
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
`

iFrameタグのallowFullScreenを指定すると動画の再生をフルスクリーンで表示することができます。

16:9の比率で縦横比を設定するお決まりの方法ですが、widthが100%に対して高さを56.25%にすると16:9になります。