LoginSignup
3
3

More than 3 years have passed since last update.

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

Posted at

はじめに

この記事はFreeCodeCampで公開されているサンプルアプリをチュートリアルした時の記録です。1
学んでみた所感であったり、英語の解釈など残していきます。
!!!もし見当違いな解釈などあれば指摘していただけると嬉しいです!!!

準備するもの

  • Node >= 6
  • API key (ここで取得してください)

プロジェクトの作成

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.jssrc/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のテキストをレンダリングする機能を実装

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行目を変更

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

3.src/App.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を書き換える。

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が呼び出されます。

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を書き換える。

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関数を3回使用しています。そのため、1つのコンポーネントに複数のuseState関数を所持できます。

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

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

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

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

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の基本的な使い方は以下の通りです。

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

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

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

スクリーンショット 2019-12-14 15.58.11.png

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

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

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3