はじめに
この記事は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.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のテキストをレンダリングする機能を実装
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行目を変更
〜省略〜
import App from './components/App'; // App.jsのパスを変更する
〜省略〜
3.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
を書き換える。
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
が呼び出されます。
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
を書き換える。
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
を変更する。
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
の基本的な使い方は以下の通りです。
const [state, dispatch] = useReducer(
reducer,
initialState
);
ReducerはinitialState
とaction
を受け取るため、action
タイプに基づいて新しい状態オブジェクトを返します。また、useEffectで、サーバーから取得しているmovies
配列としてpayload
を使用してアクションをディスパッチします。 また、検索機能では、3つの異なるアクションを実際にディスパッチしています。
以上で、ことチュートリアルで必要なプログラムの書き換えおよび知識は揃いました。ここから実行してみたところこのようになりました。
どうしてだろう。。。
どこが間違えていたか見返してみます。また、この結果は追記したいと思いますが、わかる方がいらっしゃれば、コメントをいただけたら嬉しいです。