React+hookで映画検索アプリを作る

# はじめに
この記事は[FreeCodeCamp](https://www.freecodecamp.org)で公開されている[サンプルアプリ](https://www.freecodecamp.org/news/how-to-build-a-movie-search-app-using-react-hooks-24eb72ddfaf7/)をチュートリアルした時の記録です。[^1]
学んでみた所感であったり、英語の解釈など残していきます。
！！！もし見当違いな解釈などあれば指摘していただけると嬉しいです！！！

# 準備するもの
- Node >= 6
- API key ([ここで](http://www.omdbapi.com/apikey.aspx)取得してください)

# プロジェクトの作成
Reactアプリの雛形を作成

```
$ npm install -g create-react-app
$ create-react-app hooked # "hooked"というプロジェクトが作成される
```

こんな感じのディレクトリ構成になる

```
hooked
├── README.md
├── node_modules # 省略
├── package-lock.json
├── package.json
├── public # 省略
└── src # ここにプログラムを追記していきます
    ├── App.css
    ├── App.js
    ├── App.test.js
    ├── index.css
    ├── index.js
    ├── logo.svg
    └── serviceWorker.js
```

# Componentの作成
`src`内に`components`ディレクトリを作成します。
作成した後、`src/App.js`を`src/components`内へ移動します。
続いて、
- `src/components/Header.js`
- `src/components/Movie.js`
- `src/components/Search.js`
を作成します。コードは後ほど。

## Componentの役割
ここで先に`Component`の役割について確認します。
その後、先ほど作成したファイルの役割について見ていきます。

"コンポーネント"＝"GUIのパッケージをモジュール化したもの"
GUIで使われる様々な粒度のパーツを”コンポーネント”という単位に区切って***再利用できるようにすること***、***共同作業しやすくすること***を目的としています。[^2]

## 各ファイルの役割
次に先ほど作成したファイルの役割を見ていきます。

- App.js : Header.js, Movie.js, Search.jsの親コンポーネントになります。また、ここではAPIリクエストの処理関数や、コンポーネントの初期レンダリング中にAPIを呼び出す関数を含みます。
- Header.js : アプリケーションのヘッダーをレンダリングし、タイトルのprop(プロパティ)を受け取るシンプルなコンポーネントです。
- Movie.js : 各映画情報をレンダリングします。映画のオブジェクトはpropとして渡されます。
- Search.js : 入力要素と検索ボタンを含むフォーム、入力要素の処理と、フィールドのリセットを行う関数、およびpropとして渡される検索関数を呼び出す関数を含みます。

# 作っていく
1.`src/components/Header.js`にpropのテキストをレンダリングする機能を実装

```react:src/components/Header.js
import React from "react";

const Header = (props) => {
    return (
        <header className="App-header">
            {/* propsのテキストをレンダリング */}
            <h2>{props.text}</h2>
        </header>
    );
};

export default Header;
```

2.'src/index.js`の4行目を変更

```react:src/index.js
〜省略〜
import App from './components/App';  // App.jsのパスを変更する
〜省略〜
```

3.`src/App.css`を書き換える

```css:src/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;
  }
}
```

4.続いて、`src/components/Movie.js`を書き換える。

```react:src/components/Movie.js
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;
```

この時、`movie`を引数に取っているのですが、引用元はまだ作成していないため、`movie.Poster`などの要素はまだ参照できません。
また、`DEFAULT_PLACEHOLDER_IMAGE`はAPIから取得した一部の映画に画像がないため、リンク切れの代わりにプレースホルダー画像をレンダリングするためのurlです。

5.次に`src/components/Search.js`を書き換える。
ここでは、`onChange`イベントが呼ばれると、新たな値で状態更新関数を呼び出す`handleSearchInputChanges`が呼び出されます。

```react:src/components/Search.js
import React, {useState} from "react";

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

    const handleSearchInputChanges = (e) => {
        setSearchValue("");
    };

    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`というReactのhook*機能を使用することで、クラスを記述することではなく、状態やその他のReact機能を使用することができます。
* hookとは、状態を関数コンポーネントに追加できる機能って認識でいいのかな

`useState`は初期状態を引数とし、現在の状態(≒this.state)と更新関数(≒this.setState)を含む配列を返します。

6.以上を記述したら、`src/components/App.js`を書き換える。

```react:src/components/App.js
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 = "http://www.omdbapi.com/?i=tt3896198&apikey=bc2a5cf"; // you should replace this with yours

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

    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=bc2a5cf`)
            .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;
```

ここで、`useState`関数を３回使用しています。そのため、１つのコンポーネントに複数の`useState`関数を所持できます。

useState1では、ロード状態を処理するために使用されます。
useState2では、サーバから取得した映画の配列を処理するために使用されます。
useState3では、APIリクエストを行う時に発生する可能性のあるエラーを処理するために使用されます。

また、`useEffect`フックもここで使用します。useEffectでは、関数コンポーネントにライフサイクルメソッドを追加することができます。ただ、ここは自分自身理解できていないので説明は省略します。

最初のレンダリングの後と、更新の後に`useEffect`が呼び出されます。
更新のたびに呼び出されるときは、`useEffect`関数が実行される関数と第二引数をみて、渡された変数が変更されていない場合、エフェクトの適用をスキップするようReactに指示する値を渡します。

ここで、`App.js`を変更する。

```react:src/components/App.js
import React, { useReducer, 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 initialState = {
  loading: true,
  movies: [],
  errorMessage: null
};


const reducer = (state, action) => {
  switch (action.type) {
    case "SEARCH_MOVIES_REQUEST":
      return {
        ...state,
        loading: true,
        errorMessage: null
      };
    case "SEARCH_MOVIES_SUCCESS":
      return {
        ...state,
        loading: false,
        movies: action.payload
      };
    case "SEARCH_MOVIES_FAILURE":
      return {
        ...state,
        loading: false,
        errorMessage: action.error
      };
    default:
      return state;
  }
};



const App = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

    useEffect(() => {
    
        fetch(MOVIE_API_URL)
            .then(response => response.json())
            .then(jsonResponse => {
        
            dispatch({
                type: "SEARCH_MOVIES_SUCCESS",
                payload: jsonResponse.Search
        	});
      	});
  	}, []);

    const search = searchValue => {
    	dispatch({
      	type: "SEARCH_MOVIES_REQUEST"
    	});
	
        fetch(`https://www.omdbapi.com/?s=${searchValue}&apikey=4a3b711b`)
      	.then(response => response.json())
      	.then(jsonResponse => {
        	if (jsonResponse.Response === "True") {
          	dispatch({
                type: "SEARCH_MOVIES_SUCCESS",
                payload: jsonResponse.Search
          	});
        	} else {
          	dispatch({
                type: "SEARCH_MOVIES_FAILURE",
                error: jsonResponse.Error
          	});
          }
      	});
	  };

    const { movies, errorMessage, loading } = state;

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

`useReducer`の基本的な使い方は以下の通りです。

```react:useReducer
const [state, dispatch] = useReducer(
    reducer,
    initialState
);
```

Reducerは`initialState`と`action`を受け取るため、`action`タイプに基づいて新しい状態オブジェクトを返します。また、useEffectで、サーバーから取得している`movies`配列として`payload`を使用してアクションをディスパッチします。 また、検索機能では、3つの異なるアクションを実際にディスパッチしています。

以上で、ことチュートリアルで必要なプログラムの書き換えおよび知識は揃いました。ここから実行してみたところこのようになりました。

![スクリーンショット 2019-12-14 15.58.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266431/0630c63e-6f0a-7412-0cab-97c76bc82959.png)

どうしてだろう。。。
どこが間違えていたか見返してみます。また、この結果は追記したいと思いますが、わかる方がいらっしゃれば、コメントをいただけたら嬉しいです。


# 参考にさせていただいたサイト

[^1]: [FreeCodeCamp](https://www.freecodecamp.org)
[^2]: [フロントエンドのコンポーネント設計に立ち向かう](https://qiita.com/seya/items/8814e905693f00cdade2)