今回は2019年にバズった記事、そこで紹介されていた映画検索アプリを作成しながらReactについて学んで行きたいと思います。このアプリでは日本の学習サイトではあまり解説されていない関数コンポーネントやフック、外部APIといった最新技術を網羅しているため、かなり良い学習教材になると思います。
映画検索アプリチュートリアルはこちら
↓ ↓ ↓
前提
このアプリケーションは、僕自身、React未経験の際に学習に使ったアプリケーションになります。そのためReactに触れたことがない方でも十分チャレンジできる内容かと思います。
加えて、比較的新しめの技術であるフックについても学べる内容になっているので、このアプリケーションを理解することでReactの全体像を把握することができます。
参考までに、このアプリを作成した頃の僕のレベルは以下の状態でした。
- Railsチュートリアルが大体分かる
- JavaScript・jQueryを触ったことがある
- ProgateのReactコース2周したくらい
アプリケーションの概要
今回作成するアプリケーションは、映画を検索することができるものになります。
使用技術
- 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
=> バージョンが表示されれば成功
OMDB APIは以下の手順で取得することができます。
- こちらのリンクにアクセス
- FREEを選択、メールアドレス/氏名/APIを使用するサイトの概要をそれぞれ入力
- 登録したメールアドレスに送られるリンクにアクセスし、APIをアクティベート
アプリケーションの構成
アプリケーションの作成に取り掛かる前に、全体像を把握しておきましょう。
このアプリケーションは、以下の4つのコンポーネントで構成されています。

App.js
他の3つの親コンポーネント。APIリクエストを処理する関数も含まれ、コンポーネントの初期レンダリング中にAPIを呼び出す関数が含まれる。
Header.js
ヘッダーをレンダリングし、タイトルのプロップスを受け取る。
Movie.js
各映画をレンダリング。
Search.js
入力要素と検索ボタンを含むフォーム、入力要素を処理してフィールドをリセットする関数、プロップスとして渡される検索関数を呼び出す関数を含む。
覚えておくべき用語
こちらもアプリケーション作成前に、おさらいしておきましょう。
これらを予め理解しておくことで、アプリケーション作成がグッとやりやすくなります。
component(コンポーネント)
コンポーネントにより UI を独立した再利用できる部品に分割し、部品それぞれを分離して考えることができるようになります。
アプリケーションを分解した際の部品1つ1つをコンポーネントと呼びます。先ほどの画像ではAppコンポーネントを3つのコンポーネント(部品)に分解していますね。

props(プロップス)
親コンポーネントから子コンポーネントへ値を渡すためのデータのこと。
以下のようにpropsと呼ばれるデータを渡すことで、タイトルや映画画像の表示を変えることができます。子コンポーネントに渡されるタイトルや映画画像を含んだデータのことをpropsと呼びます。

state(ステート)
Stateとはコンポーネント利用時に設定ができる値で、Propsと違い後から変更ができます。 基本的にはコンポーネントの作成時にconstructorメソッド内でthis.stateオブジェクトに値を指定しておくことで、他のメソッドからthis.stateとして取得することが可能です。
propsと似ていますが大きく違いがあります。主な違いについて見ていきましょう。
- stateはクラスコンポーネントでのみ使用可能。
- propsは不変のデータ(immutable)だが、stateは可変のデータ(mutable)。
- propsは親コンポーネントから渡されるデータだが、stateはコンポーネント自身が保持しているデータ。(コンポーネントからコンポーネントへ渡されることもない)
このようにpropsとstateには違いがあるため、場面によって使い分けることが一番の理想と言えそうです。
hooks(フック)
状態管理などのReactの機能を、クラスを書かずに使えるようになる機能、それがフックです。フックとは関数で、React16.8(2019/2~)で追加された機能となります。
これまでクラスコンポーネントで書かなければならなかった記述が、フックの登場で関数コンポーネントでも記述することができるようになりました。これにより、より短くシンプルなコードが可能になりました。
基本となるフックは「useState」「useEffect」「useContext」の3つになります。これらはマストで押さえておきましょう。
ライフサイクル
ライフサイクルとライフサイクルメソッドについてはこちらの記事が圧倒的に分かりやすかったため、今回はこちらを引用して説明していきます。
Reactのコンポーネントにはライフサイクルと呼ばれる時間の流れがあります。朝日が昇り、日中に活動し、夜になり就寝するというイメージです。
朝日が昇ることをMounting、日中に活動することをUpdating、夜になり就寝することをUnmountingと呼びます。

それぞれのライフサイクルで、コンポーネントが行なっていることを簡単にまとめると以下のようになります。
Mounting | コンポーネントがユーザーにレンダリングされるまでの仕込みの期間 |
Updating | コンポーネントがユーザに表示されており、ユーザーが操作できる期間 |
Unmounting | 他のコンポーネントに切り替え前に現在表示されているコンポーネントを破棄するための期間 |
ライフサイクルメソッド
先ほど説明したライフサイクルには、それに付随するライフサイクルメソッドというものが存在します。これらのメソッドは順番に呼ばれます。
それぞれのライフサイクルで実行されるメソッドをまとめると以下のようになります。
Mounting | constructor() ↓ getDerivedFromProps() ↓ render() ↓ componentDidMount() |
Updating | getDerivedStateFromProps() ↓ shouldComponentUpdate() ↓ render() ↓ getSnapshotBeforeUpdate() ↓ componentDidUpdate() |
Unmounting | componentWillUnmount() |
アプリケーション作成
ではここから実際に映画検索アプリの作成に取り掛かっていきます。
新規アプリケーションの立ち上げ
まずは以下のコマンドで新規Reactアプリを立ち上げましょう。アプリケーション名はhookedとしています。
% create-react-app hooked
上記コマンドでアプリケーションが作成されない場合は、create-react-appがインストールされていない可能性があります。その場合は、以下のコマンドを打ち込み、その後上記コマンドを入力しましょう。
% npm install -g create-react-app
componentsフォルダの作成
srcディレクトリ直下にcomponentsフォルダを作成しましょう。この中にコンポーネントを入れていく形になります。

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

index.jsの変更
App.jsを移動させたので、index.jsのApp.jsを読み込む記述を修正しなければなりません。以下の記述を変更しましょう。
import App from './App';
↓ 以下に修正
import App from './components/App';
コンポーネントの作成
では、残り3つのコンポーネントをcomponentsフォルダに作成します。

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 thetext
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;

以下の記述では、映画のタイトル(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);
では実際のコードを確認してみましょう。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)を実行することができます。

useEffectの第一引数であるfetchでは、JSON形式の映画データをAPIから取得しています。その後、useStateを更新しています。
fetch(MOVIE_API_URL)
.then(response => response.json())
.then(jsonResponse => {
setMovies(jsonResponse.Search);
setLoading(false);
});

次は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を実行して以下のような状態になれば成功です。
参考
- 2020年のフロントエンドマスターになりたければこの9プロジェクトを作れ
- How to build a movie search app using React Hooks
- OMDb API
- 【Reactの設計を学ぶ】ライフサイクルを知ろう
今回は2019年にバズった記事、そこで紹介されていた映画検索アプリを作成&解説しました。関数コンポーネントやフックがたくさん使われており、かなり参考になる学習アプリだと思います。ぜひチャレンジしてみてください。