React

【React超おすすめ学習法】映画検索アプリを作成しながらReactをマスターしよう!

React

 

今回は2019年にバズった記事、そこで紹介されていた映画検索アプリを作成しながらReactについて学んで行きたいと思います。このアプリでは日本の学習サイトではあまり解説されていない関数コンポーネントやフック、外部APIといった最新技術を網羅しているため、かなり良い学習教材になると思います。

フックは、React16.8(2019/2~)で追加された機能になります。

映画検索アプリチュートリアルはこちら
↓ ↓ ↓

前提

このアプリケーションは、僕自身、React未経験の際に学習に使ったアプリケーションになります。そのためReactに触れたことがない方でも十分チャレンジできる内容かと思います。

加えて、比較的新しめの技術であるフックについても学べる内容になっているので、このアプリケーションを理解することでReactの全体像を把握することができます。

参考までに、このアプリを作成した頃の僕のレベルは以下の状態でした。

  • Railsチュートリアルが大体分かる
  • JavaScript・jQueryを触ったことがある
  • ProgateのReactコース2周したくらい
ProgateのReactコースは必ずやっておきましょう!

アプリケーションの概要

今回作成するアプリケーションは、映画を検索することができるものになります。

Image from Gyazo

使用技術

  • React with Hooks(フックを用いたReact)
  • create-react-app(rails newのようなコマンド)
  • JSX(HTMLのようなJavaScriptの拡張機能)
  • CSS

必要なもの

  • Node (≥ 6)
  • テキストエディタ(VScodeがおすすめ)
  • OMDB API(映画情報を取得する際に必要なAPI)

 

Nodeは以下のコマンドで入手することができます。

% brew install nodejs
% node -v
=> バージョンが表示されれば成功
Macユーザーを想定しているため、Homebrewを用いてインストールを行なっています。

 

OMDB APIは以下の手順で取得することができます。

  1. こちらのリンクにアクセス
  2. FREEを選択、メールアドレス/氏名/APIを使用するサイトの概要をそれぞれ入力
  3. 登録したメールアドレスに送られるリンクにアクセスし、APIをアクティベート
APIを使用するサイトの概要は適当に記入していただいて大丈夫です。

アプリケーションの構成

アプリケーションの作成に取り掛かる前に、全体像を把握しておきましょう。

このアプリケーションは、以下の4つのコンポーネントで構成されています。

コンポーネント構造

App.js

他の3つの親コンポーネント。APIリクエストを処理する関数も含まれ、コンポーネントの初期レンダリング中にAPIを呼び出す関数が含まれる。

Header.js

ヘッダーをレンダリングし、タイトルのプロップスを受け取る。

Movie.js

各映画をレンダリング。

Search.js

入力要素と検索ボタンを含むフォーム、入力要素を処理してフィールドをリセットする関数、プロップスとして渡される検索関数を呼び出す関数を含む。

覚えておくべき用語

こちらもアプリケーション作成前に、おさらいしておきましょう。

これらを予め理解しておくことで、アプリケーション作成がグッとやりやすくなります。

component(コンポーネント)

コンポーネントにより UI を独立した再利用できる部品に分割し、部品それぞれを分離して考えることができるようになります。

参考:コンポーネントとprops

 

アプリケーションを分解した際の部品1つ1つをコンポーネントと呼びます。先ほどの画像ではAppコンポーネントを3つのコンポーネント(部品)に分解していますね。

コンポーネント構造

props(プロップス)

親コンポーネントから子コンポーネントへ値を渡すためのデータのこと。

参考:コンポーネントとprops

 

以下のようにpropsと呼ばれるデータを渡すことで、タイトルや映画画像の表示を変えることができます。子コンポーネントに渡されるタイトルや映画画像を含んだデータのことをpropsと呼びます。

propsの仕組み

state(ステート)

Stateとはコンポーネント利用時に設定ができる値で、Propsと違い後から変更ができます。 基本的にはコンポーネントの作成時にconstructorメソッド内でthis.stateオブジェクトに値を指定しておくことで、他のメソッドからthis.stateとして取得することが可能です。

参考:Stateとは? – React入門

 

propsと似ていますが大きく違いがあります。主な違いについて見ていきましょう。

  • stateはクラスコンポーネントでのみ使用可能。
  • propsは不変のデータ(immutable)だが、stateは可変のデータ(mutable)。
  • propsは親コンポーネントから渡されるデータだが、stateはコンポーネント自身が保持しているデータ。(コンポーネントからコンポーネントへ渡されることもない)

 

このようにpropsとstateには違いがあるため、場面によって使い分けることが一番の理想と言えそうです。

参考:ReactにおけStateとPropsの違い

hooks(フック)

状態管理などのReactの機能を、クラスを書かずに使えるようになる機能、それがフックです。フックとは関数で、React16.8(2019/2~)で追加された機能となります。

参考:5分でわかるReact Hooks

 

これまでクラスコンポーネントで書かなければならなかった記述が、フックの登場で関数コンポーネントでも記述することができるようになりました。これにより、より短くシンプルなコードが可能になりました。

基本となるフックは「useState」「useEffect」「useContext」の3つになります。これらはマストで押さえておきましょう。

ライフサイクル

ライフサイクルとライフサイクルメソッドについてはこちらの記事が圧倒的に分かりやすかったため、今回はこちらを引用して説明していきます。

Reactのコンポーネントにはライフサイクルと呼ばれる時間の流れがあります。朝日が昇り、日中に活動し、夜になり就寝するというイメージです。

朝日が昇ることをMounting、日中に活動することをUpdating、夜になり就寝することをUnmountingと呼びます。

引用画像

引用元:【Reactの設計を学ぶ】ライフサイクルを知ろう

 

それぞれのライフサイクルで、コンポーネントが行なっていることを簡単にまとめると以下のようになります。

Mountingコンポーネントがユーザーにレンダリングされるまでの仕込みの期間
Updatingコンポーネントがユーザに表示されており、ユーザーが操作できる期間
Unmounting他のコンポーネントに切り替え前に現在表示されているコンポーネントを破棄するための期間

ライフサイクルメソッド

先ほど説明したライフサイクルには、それに付随するライフサイクルメソッドというものが存在します。これらのメソッドは順番に呼ばれます。

ライフサイクルメソッドはクラスコンポーネントでしか使用できません。関数コンポーネントではuseEffectと呼ばれるフックを用いることで一部代用ができます。

 

それぞれのライフサイクルで実行されるメソッドをまとめると以下のようになります。

Mountingconstructor()

getDerivedFromProps()

render()

componentDidMount()
UpdatinggetDerivedStateFromProps()

shouldComponentUpdate()

render()

getSnapshotBeforeUpdate()

componentDidUpdate()
UnmountingcomponentWillUnmount()
useEffectはライフサイクルメソッドであるcomponentDidMount, componentDidUpdate, componentWillUnmountが3つ合わさったものです。

アプリケーション作成

ではここから実際に映画検索アプリの作成に取り掛かっていきます。

新規アプリケーションの立ち上げ

まずは以下のコマンドで新規Reactアプリを立ち上げましょう。アプリケーション名はhookedとしています。

% create-react-app hooked

上記コマンドでアプリケーションが作成されない場合は、create-react-appがインストールされていない可能性があります。その場合は、以下のコマンドを打ち込み、その後上記コマンドを入力しましょう。

% npm install -g create-react-app

componentsフォルダの作成

srcディレクトリ直下にcomponentsフォルダを作成しましょう。この中にコンポーネントを入れていく形になります。

componetsフォルダの作成

 

componentsフォルダ作成後、App.jsをcomponentsフォルダの直下に移動させましょう。

App.jsの移動

index.jsの変更

App.jsを移動させたので、index.jsのApp.jsを読み込む記述を修正しなければなりません。以下の記述を変更しましょう。

import App from './App';

↓ 以下に修正

import App from './components/App';

コンポーネントの作成

では、残り3つのコンポーネントをcomponentsフォルダに作成します。

3つのコンポーネントを作成

App.cssの編集

続いてはCSSをあてていきます。

以下のコードをそのままApp.cssに貼り付けましょう。

.App {
  text-align: center;
}

.App-header {
  background-color: #282c34;
  height: 70px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
  padding: 20px;
  cursor: pointer;
}

.spinner {
  height: 80px;
  margin: auto;
}

.App-intro {
  font-size: large;
}

/* new css for movie component */

* {
  box-sizing: border-box;
}

.movies {
  display: flex;
  flex-wrap: wrap;
  flex-direction: row;
}

.App-header h2 {
  margin: 0;
}

.add-movies {
  text-align: center;
}

.add-movies button {
  font-size: 16px;
  padding: 8px;
  margin: 0 10px 30px 10px;
}

.movie {
  padding: 5px 25px 10px 25px;
  max-width: 25%;
}

.errorMessage {
  margin: auto;
  font-weight: bold;
  color: rgb(161, 15, 15);
}


.search {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: center;
  margin-top: 10px;
}


input[type="submit"] {
  padding: 5px;
  background-color: transparent;
  color: black;
  border: 1px solid black;
  width: 80px;
  margin-left: 5px;
  cursor: pointer;
}


input[type="submit"]:hover {
  background-color: #282c34;
  color: antiquewhite;
}


.search > input[type="text"]{
  width: 40%;
  min-width: 170px;
}

@media screen and (min-width: 694px) and (max-width: 915px) {
  .movie {
    max-width: 33%;
  }
}

@media screen and (min-width: 652px) and (max-width: 693px) {
  .movie {
    max-width: 50%;
  }
}


@media screen and (max-width: 651px) {
  .movie {
    max-width: 100%;
    margin: auto;
  }
}

Header.jsの編集

ここから各コンポーネントに実際にコードを書いていく作業になります。

まずはHeaderコンポーネントです。

Header.jsに以下のコードを記述(or 貼り付け)しましょう。

import React from "react";

const Header = (props) => {
  return (
    <header className="App-header">
      <h2>{props.text}</h2>
    </header>
  );
};

export default Header;

 

このコードの解説を見ていきます。

This component doesn’t require that much of an explanation — it’s basically a functional component that renders the header tag with the text props.

このコンポーネントはそれほど多くの説明を必要としません — 基本的にはtext propsでヘッダータグをレンダリングする機能コンポーネントです。

 

説明がほとんど書かれていませんが、ヘッダーのhtml要素をreturnしていることが分かれば十分です。ちなみに「export default Header;」のように、コンポーネントをexportしないと外部で使用できないので注意しましょう。

Movie.jsの編集

次にMovie.jsの編集です。

Movie.jsに以下のコードを記述(or 貼り付け)しましょう。

import React from "react";

const DEFAULT_PLACEHOLDER_IMAGE =
  "https://m.media-amazon.com/images/M/MV5BMTczNTI2ODUwOF5BMl5BanBnXkFtZTcwMTU0NTIzMw@@._V1_SX300.jpg";


const Movie = ({ movie }) => {
  const poster =
    movie.Poster === "N/A" ? DEFAULT_PLACEHOLDER_IMAGE : movie.Poster;
  return (
    <div className="movie">
      <h2>{movie.Title}</h2>
      <div>
        <img
          width="200"
          alt={`The movie titled: ${movie.Title}`}
          src={poster}
        />
      </div>
      <p>({movie.Year})</p>
    </div>
  );
};


export default Movie;

 

コードを順番に見ていきましょう。

APIから取得した映画の一部には画像がないため、その代わりとしてプレースホルダー画像をレンダリングする必要があります。このプレースホルダ画像が「DEFAULT_PLACEHOLDER_IMAGE」に格納されています。

const DEFAULT_PLACEHOLDER_IMAGE =
  "https://m.media-amazon.com/images/M/MV5BMTczNTI2ODUwOF5BMl5BanBnXkFtZTcwMTU0NTIzMw@@._V1_SX300.jpg";

 

「条件式 ? 式1 : 式2」は三項演算子と呼ばれ、if文を用いた条件分岐と同じになります。簡単に解説すると、Poster PropsがN/A(Not Assign)だった場合には「DEFAULT_PLACEHOLDER_IMAGE」が、N/Aではなかった場合は「movie.Poster」が実行されます。詳しくは以下の記事をご覧ください。

const poster =
  movie.Poster === "N/A" ? DEFAULT_PLACEHOLDER_IMAGE : movie.Poster;
JavaScript
【JavaScript】条件分岐をよりシンプルに記述することができる三項演算子について解説if文を用いた条件分岐をよりシンプルに記述することができる三項演算子について解説しています。今後条件分岐を記述する場合は、ぜひ三項演算子を用いていただけたらと思います。...

 

以下の記述では、映画のタイトル(movie.Title)、映画画像(poster)、公開年度(movie.Year)をreturnしていますね。

return (
  <div className="movie">
    <h2>{movie.Title}</h2>
    <div>
      <img
        width="200"
        alt={`The movie titled: ${movie.Title}`}
        src={poster}
      />
    </div>
    <p>({movie.Year})</p>
  </div>
);

Search.jsの編集

続いて、検索コンポーネントであるSearch.jsを編集していきましょう。

Search.jsに以下のコードを記述(or 貼り付け)しましょう。

import React, { useState } from "react";


const Search = (props) => {
  const [searchValue, setSearchValue] = useState("");

  const handleSearchInputChanges = (e) => {
    setSearchValue(e.target.value);
  }

  const resetInputField = () => {
    setSearchValue("")
  }

  const callSearchFunction = (e) => {
    e.preventDefault();
    props.search(searchValue);
    resetInputField();
  }

  return (
      <form className="search">
        <input
          value={searchValue}
          onChange={handleSearchInputChanges}
          type="text"
        />
        <input onClick={callSearchFunction} type="submit" value="SEARCH" />
      </form>
    );
}

export default Search;

 

ここから急に難しくなりましたね。

まずはフックであるuseStateの定義を見ていきましょう。useStateとは、ステートフルな値とそれを更新するための関数を返す機能です。setStateはstateを更新する関数であり、initialStateはstateの初期値が格納されている箇所になります。

const [state, setState] = useState(initialState);

参考:Using the State Hook

 

では実際のコードを確認してみましょう。setSearchValueはsearchValueを更新する関数であり、initialStateは””(空)とすることで何も検索されていない状態を表しています。

const [searchValue, setSearchValue] = useState("");

 

それぞれの関数も見ていきましょう。

こちらはform内の値が変更された時に発火するメソッドになります。stateの値をform内に記述された値に変換しています。

const handleSearchInputChanges = (e) => {
  setSearchValue(e.target.value);
}

 

resetInputFieldメソッドは入力値の値をリセットしています。

const resetInputField = () => {
  setSearchValue("")
}

 

callSearchFunctionメソッドについては3つの処理を行なっています。

「e.preventDefault()」はブラウザのデフォルト動作を阻害しています。具体的には、actionで指定されたURLへのページ遷移+データ送信を阻害していることになります。

参考:JavaScriptのpreventDefault()って難しくない?preventDefault()を使うための前提知識

「props.search(searchValue);」ではformに入力された文字を引数にとり、search関数を実行しています。

「resetInputField();」では入力値の値をリセットする関数を呼び出しています。

const callSearchFunction = (e) => {
  e.preventDefault();
  props.search(searchValue);
  resetInputField();
}

 

そして以下の結果を返します(retrun)。

簡単に言うと、formに値が入力される度にsearchValueの値が更新されていき、submitボタンが押されることでその値を用いて検索が行われるということになります。

return (
  <form className="search">
    <input
      value={searchValue}
      onChange={handleSearchInputChanges}
      type="text"
    />
    <input onClick={callSearchFunction} type="submit" value="SEARCH" />
  </form>
);

App.jsの編集

最後に、3つのコンポーネント(Header/Movie/Search)の親コンポーネントであるApp.jsを編集していきます。

App.jsに以下のコードを記述(or 貼り付け)しましょう。

import React, { useState, useEffect } from "react";
import "../App.css";
import Header from "./Header";
import Movie from "./Movie";
import Search from "./Search";


const MOVIE_API_URL = "https://www.omdbapi.com/?s=man&apikey=4a3b711b";


const App = () => {
  const [loading, setLoading] = useState(true);
  const [movies, setMovies] = useState([]);
  const [errorMessage, setErrorMessage] = useState(null);

  useEffect(() => {
    fetch(MOVIE_API_URL)
      .then(response => response.json())
      .then(jsonResponse => {
        setMovies(jsonResponse.Search);
        setLoading(false);
      });
  }, []);

  const search = searchValue => {
    setLoading(true);
    setErrorMessage(null);

    fetch(`https://www.omdbapi.com/?s=${searchValue}&apikey=4a3b711b`)
      .then(response => response.json())
      .then(jsonResponse => {
        if (jsonResponse.Response === "True") {
          setMovies(jsonResponse.Search);
          setLoading(false);
        } else {
          setErrorMessage(jsonResponse.Error);
          setLoading(false);
        }
      });
    };


    return (
     <div className="App">
      <Header text="HOOKED" />
      <Search search={search} />
      <p className="App-intro">Sharing a few of our favourite movies</p>
      <div className="movies">
        {loading && !errorMessage ? (
         <span>loading...</span>
         ) : errorMessage ? (
          <div className="errorMessage">{errorMessage}</div>
        ) : (
          movies.map((movie, index) => (
            <Movie key={`${index}-${movie.Title}`} movie={movie} />
          ))
        )}
      </div>
    </div>
  );
};


export default App;
個人的にはここが一番理解に苦しんだ箇所です。

 

順番に見ていきましょう。

まずは以下のURLが映画情報を扱うAPIになります。

const MOVIE_API_URL = "https://www.omdbapi.com/?s=man&apikey=4a3b711b";

 

続いて3つのuseStateが登場しています。

setStateはstateを更新する関数、initialStateは初期値が格納されている箇所でしたね。

const [state, setState] = useState(initialState);

 

それぞれのuseStateの役割をまとめてみると以下のようになります。

  • 1つ目は読み込みの状態を処理
  • 2つ目はサーバから取得した映画を処理
  • 3つ目はAPIリクエストのエラー状態を処理
const [loading, setLoading] = useState(true);
const [movies, setMovies] = useState([]);
const [errorMessage, setErrorMessage] = useState(null);

 

次にフックの1つであるuseEffectの記述について見ていきましょう。

useEffect(() => {
  fetch(MOVIE_API_URL)
    .then(response => response.json())
    .then(jsonResponse => {
      setMovies(jsonResponse.Search);
      setLoading(false);
    });
}, []);

 

ポイントはuseEffectの第二引数が空配列になっている点です。

useEffectの第二引数を空配列にすることで、初回のレンダリング時に第一引数の関数(今回でいうfetch)を実行することができます。

React
【React Hooks】分かりそうで分からないuseEffectの第二引数について簡単にまとめてみた分かりそうで分からないuseEffectの第二引数について解説しています。useEffectの第二引数は、第一引数に渡された関数の実行タイミングをコントロールする役割があります。ぜひこの機会に理解を深めていただけたらと思います。...

 

useEffectの第一引数であるfetchでは、JSON形式の映画データをAPIから取得しています。その後、useStateを更新しています。

fetch(MOVIE_API_URL)
  .then(response => response.json())
  .then(jsonResponse => {
    setMovies(jsonResponse.Search);
    setLoading(false);
});
JavaScript
【JavaScript】非同期処理を行うfetchについて初心者向けにまとめてみたJavaScriptの非同期処理における、fetchについて初心者向けに解説しています。英単語の意味からも何を行うメソッドか判断がつくとは思いますが、この機会に意味と使い方をしっかりと理解しておきましょう。...

 

次はsearch関数を見ていきます。引数としてsearchValueを取っていますね。

const search = searchValue => {
  setLoading(true);
  setErrorMessage(null);

  fetch(`https://www.omdbapi.com/?s=${searchValue}&apikey=4a3b711b`)
    .then(response => response.json())
    .then(jsonResponse => {
      if (jsonResponse.Response === "True") {
        setMovies(jsonResponse.Search);
        setLoading(false);
      } else {
        setErrorMessage(jsonResponse.Error);
        setLoading(false);
      }
  });
};

 

忘れているとは思いますが、search関数はSearch.jsの中で出てきていました。簡単に言うと、検索ボタンが押された際、入力された値を引数にsearch関数が実行されるようになっています。

// 該当部分のみ抜粋

const callSearchFunction = (e) => {
  e.preventDefault();
  props.search(searchValue);
  resetInputField();
}

 

searchの中身を見ていきます。

fetch(`https://www.omdbapi.com/?s=${searchValue}&apikey=4a3b711b`)
  .then(response => response.json())
  .then(jsonResponse => {
    if (jsonResponse.Response === "True") {
      setMovies(jsonResponse.Search);
      setLoading(false);
    } else {
      setErrorMessage(jsonResponse.Error);
      setLoading(false);
    }
});

 

fetchについては先ほどと同じなので省略します。

以下の記述では検索結果があった場合となかった場合を条件分岐しています。検索結果があった場合はその結果を用いてuseStateを更新し、検索結果がなかった場合はエラー状態のuseStateを更新しています。

if (jsonResponse.Response === "True") {
  setMovies(jsonResponse.Search);
  setLoading(false);
} else {
  setErrorMessage(jsonResponse.Error);
  setLoading(false);
}

 

最後にreturn部分で映画データを表示しています。

  • loadingがtrueかつ、errorMessageが存在していない→loadingを表示
  • errorMessageが存在→errorMessageを表示
  • それ以外→映画データを表示
return (
  <div className="App">
    <Header text="HOOKED" />
    <Search search={search} />
    <p className="App-intro">Sharing a few of our favourite movies</p>
    <div className="movies">
      {loading && !errorMessage ? (
       <span>loading...</span>
       ) : errorMessage ? (
        <div className="errorMessage">{errorMessage}</div>
       ) : (
        movies.map((movie, index) => (
          <Movie key={`${index}-${movie.Title}`} movie={movie} />
       ))
       )}
    </div>
  </div>
);

 

これでコードが完成です。

npm startを実行して以下のような状態になれば成功です。

Image from Gyazo

参考

 

 

今回は2019年にバズった記事、そこで紹介されていた映画検索アプリを作成&解説しました。関数コンポーネントやフックがたくさん使われており、かなり参考になる学習アプリだと思います。ぜひチャレンジしてみてください。

初見だと理解するのはかなり難しいですが、とりあえずは真似して書いてみてください。