基於電影API,結合 Reactjs 的 hooks 功能,製作的 demo.
參考文章 how-to-build-a-movie-search-app-using-react-hooks,也能算得上一篇翻譯吧,但是主要是根據文中的內容,自己實際來做了一個真正的 demo 出來,還是有一些擴展的.就寫成原創了,如有不妥,請告知修改.
1. 生成 app
使用 create-react-app movies-react-hooks
初始化 app,如果沒有安裝 create-react-app
那麼可以使用:
npm install -g create-react-app
來進行安裝.
生成之後我們將看到
有了基礎目錄結構,我們就可以開始了.
2. 準備工作與Header組件
在這個項目中我們需要四個組件:
- App.js
- Header.js
- Movie.js
- Search.js
現在,我們在 src
文件夾內創建一個新的文件夾 src/components
文件夾,然後將 src/App.js
文件移動到 src/components/App.js
(如果這時候運行項目,注意 App.js
文件中的引用的路徑),並且創建一個新的文件 src/components/Header.js
:
// Header.js
import React from "react";
const Header = (props) => {
return (
<header classNames="App-header">
<h2>{props.text}</h2>
</header>
);
};
export default Header;
同時,我們修改 src/components/App.js
爲:
import React from 'react';
import './App.css';
import Header from './Header';
function App() {
return (
<div className="App">
<Header text="movie app" />
</div>
);
}
export default App;
由於我們移動了 App.js
所以我們需要將 src/index.js
修改爲:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './components/App'; // 這行我們修改了引用路徑
import * as serviceWorker from './serviceWorker';
ReactDOM.render(<App />, document.getElementById('root'));
serviceWorker.unregister();
在此項目中打開命令行,運行項目:
npm start
可以看到
我們開始寫部分樣式,讓 Header
組件看起來更header.
/* src/components/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;
}
保存文件後我們就可以在 localhost:3000
中看到頁面發生了變化.
3. Movie 組件
同樣的,創建 src/components/Movis.js
文件:
import React from 'react';
const DEFAULT_PLACEHOLDER_IMAGE = "https://m.media-amazon.com/images/M/MV5BMTczNTI2ODUwOF5BMl5BanBnXkFtZTcwMTU0NTIzMw@@._V1_SX300.jpg";
const Movie = ({ movie }) => {
return (
<div className="movie">
<h2>銀翼殺手</h2>
<div>
<img width="200" alt={`The movie titled: 銀翼殺手`}
src={DEFAULT_PLACEHOLDER_IMAGE} />
</div>
<p>2018-10-10</p>
</div>
);
};
export default Movie;
只是簡單的將影片信息展示出來,目前我們的影片信息都是固定的,後期會根據請求回來的數據進行修改,同時修改 src/components/App.js
:
import React from 'react';
import './App.css';
import Header from './Header';
import Movie from './Movie'
function App() {
return (
<div className="App">
<Header text="movie app" />
<Movie />
</div>
);
}
export default App;
JavaScript 書寫的差不多了我們在將樣式添加上去:
/* src/components/App.css*/
/* ... */
* {
box-sizing: border-box;
}
@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;
}
}
保存以上文件,我們可以看到 localhost:3000
發生了變化:
4. 請求 movie 數據
現在先將我們的組件進程放在一邊,先去請求 movie 數據,讓頁面看起來更真實.
讓我們去修改 src/components/App.js
:
import React, { useState, useEffect } from 'react';
import './App.css';
import Header from './Header';
import Movie from './Movie'
const MOVIE_API_URL = "https://www.omdbapi.com/?s=man&apikey=5abd63d1";
function 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);
});
}, []);
return (
<div className="App">
<Header text="Movie app" />
<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;
同時,我們修改樣式:
/* App.css*/
/* ... */
.App-header h2 {
margin: 0;
}
.movies {
display: flex;
flex-wrap: wrap;
flex-direction: row;
}
.movie {
padding: 5px 25px 10px 25px;
max-width: 25%;
}
我們可以看到在某個循環內部我們調用了 Movie
組件,並傳遞了 movie
屬性.我們來修改上一步在 movie
組件內部寫死的數據:
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)沒有海報時顯示默認海報.
現在我們刷新頁面,可以看到新的電影信息已經請求完畢.
讓我們回到 App.js
文件,相信你也看到了我們使用了一些新的 hooks
:useState
以及 useEffect
. 這也是我們這個 demo 所重點展示的內容,先來看看官網對 hooks
以及這兩個新 hooks
的介紹:
Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。
useState
通過在函數組件裏調用它來給組件添加一些內部 state.useState
會返回一對值:當前狀態和一個讓你更新它的函數,你可以在事件處理函數中或其他一些地方調用這個函數。它類似 class 組件的 this.setState.
useEffect
Effect Hook 可以讓你在函數組件中執行副作用操作.如果你熟悉 React class 的生命週期函數,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 這三個函數的組合。
先來詳細的說一下 useState
:
接受唯一的一個參數,爲初始 state.在上面的例子中我們可以看到:
const [loading, setLoading] = useState(true);
const [movies, setMovies] = useState([]);
我們在一個函數組件中調用了兩次 useState
hook,第一次相當於我們定義了一個 loading state ,並將它的初始值設置爲 true
(useState
的參數),並定義了更新此state 的 setLoading
方法.第二次同理. 現在我們使用等價的 class 示例來展示 useState
hook的使用:
// App.js
import React, { useState, useEffect } from 'react';
import './App.css';
import Header from './Header';
import Movie from './Movie'
const MOVIE_API_URL = "https://www.omdbapi.com/?s=man&apikey=5abd63d1";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: true,
movies: [],
errorMessage: null
}
}
componentDidMount() {
fetch(MOVIE_API_URL)
.then(response => response.json())
.then(jsonResponse => {
this.setState({
movies: jsonResponse.Search,
loading: false
})
});
}
render() {
return (
<div className="App" >
<Header text="Movie App" />
<div className="movies">
{this.state.loading && !this.state.errorMessage ?
(<span>loading</span>) :
this.state.errorMessage ?
(<div className="errorMessage">
{this.state.errorMessage}
</div>) : (this.state.movies.map((movie, index) => (
<Movie key={`${index}-${movie.Title}`}
movie={movie} />
)))}
</div>
</div>
)
}
}
export default App;
先主要看 constructor
函數中我們通過 this.state
定義了三個state
: loading
:負責頁面的加載狀態,movies
用於存儲請求來的電影數據,errorMessage
:保存請求失敗後的一些錯誤信息.並賦予了初始值.而後在 componentDidMount
聲明週期函數中我們請求數據,並使用 this.setState
修改了我們定義的某些 state
.
通過兩者的比較,我們發現 useState
hook 大大精簡了我們設置 state
以及更新 state
的方式,而 useEffect
則是精簡了我們在生命週期函數中重述書寫某些邏輯的語句.
5. Search 組件
讓我們爲此 demo 添加 Search
組件:
// components/Search.js
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.preventDefault();
props.search(searchValue);
resetInputField();
}
return (
<form className="search">
<input
value={searchValue}
onChange={handleSearchInputChanges}
type="text"
/>
<input type="submit" onClick={callSearchFunction} value="SEARCH" />
</form>
)
}
export default Search;
同時,修改 css 樣式:
/* App.css */
.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;
}
爲 App.js
添加相關邏輯:
// App.js
import React, { useState, useEffect } from 'react';
import './App.css';
import Header from './Header';
import Movie from './Movie';
import Search from './Search'; // new
const MOVIE_API_URL = "https://www.omdbapi.com/?s=jackie&apikey=5abd63d1";
function 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);
});
}, []);
// new function
const search = searchValue => {
setLoading(true);
setErrorMessage(null);
fetch(`https://www.omdbapi.com/?s=${searchValue}&apikey=5abd63d1`)
.then(res => res.json())
.then(jsonRes => {
if (jsonRes.Response === "True") {
setMovies(jsonRes.Search);
} else {
setErrorMessage(jsonRes.Error);
}
setLoading(false)
})
}
return (
<div className="App">
<Header text="Movie App" />
<Search search={search} />
<p className="App-intro">分享快樂,從電影開始</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;
包含 css 的調整,可以直接看 源碼地址 .
這裏主要說一下添加的一些東西.主要是 Search
組件的添加,以及點擊 SEAECH
按鈕時發送請求的邏輯,我們將邏輯請求放在的父組件中來做,這是基本的組件設計原則.因爲另外一個子組件 Movie
需要用到我們請求返回的數據.
同樣的,我們附上 class component
版本的App.js
:
import React 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=5abd63d1";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: true,
movies: [],
errorMessage: null
}
this.search = this.search.bind(this);
}
search(searchValue) {
console.log(this)
this.setState({
loading: true,
errorMessage: null
})
fetch(`https://www.omdbapi.com/?s=${searchValue}&apikey=5abd63d1`)
.then(res => res.json())
.then(jsonRes => {
if (jsonRes.Response === "True") {
this.setState({
loading: false,
movies: jsonRes.Search
})
} else {
this.setState({
loading: false,
errorMessage: jsonRes.Error
})
}
})
}
componentDidMount() {
fetch(MOVIE_API_URL)
.then(response => response.json())
.then(jsonResponse => {
this.setState({
movies: jsonResponse.Search,
loading: false
})
});
}
render() {
return (
<div className="App" >
<Header text="Movie App" />
<Search search={this.search} />
<div className="movies">
{this.state.loading && !this.state.errorMessage ?
(<span>loading</span>) :
this.state.errorMessage ?
(<div className="errorMessage">
{this.state.errorMessage}
</div>) : (this.state.movies.map((movie, index) => (
<Movie key={`${index}-${movie.Title}`}
movie={movie} />
)))}
</div>
</div>
)
}
}
export default App;
最大的不同也是事件處理需要綁定 this
.
到這裏應該說一個基本的 hooks
演示demo已經結束了,但是我們想更近一步,使用 useReducer
來替換 useState
,我們先來看看官方對 useReducer
的定義:
const [state, dispatch] = useReducer(reducer, initialArg, init);
useState 的替代方案。它接收一個形如 (state, action) => newState 的 reducer,並返回當前的 state 以及與其配套的 dispatch 方法。(如果你熟悉 Redux 的話,就已經知道它如何工作了。)
在某些場景下,useReducer 會比 useState 更適用,例如 state 邏輯較複雜且包含多個子值,或者下一個 state 依賴於之前的 state 等。並且,使用 useReducer 還能給那些會觸發深更新的組件做性能優化,因爲你可以向子組件傳遞 dispatch 而不是回調函數 。
具體到本項目中的使用呢,就是:
// 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=jackie&apikey=5abd63d1";
const initialState = {
loading: true,
movies: [],
errorMessage: null
}
function reducer(state, action) {
switch (action.type) {
case 'requesting':
return {
...state,
loading: true
};
case 'query_success':
return {
...state,
loading: false,
movies: action.movies
};
case 'query_error':
return {
...state,
loading: false,
errorMessage: action.error
};
default:
return state;
}
}
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
fetch(MOVIE_API_URL)
.then(response => response.json())
.then(jsonResponse => {
dispatch({
type: "query_success",
movies: jsonResponse.Search
})
});
}, []);
const search = searchValue => {
dispatch({
type: "requesting"
})
fetch(`https://www.omdbapi.com/?s=${searchValue}&apikey=5abd63d1`)
.then(res => res.json())
.then(jsonRes => {
if (jsonRes.Response === "True") {
dispatch({
type: "query_success",
movies: jsonRes.Search
})
} else {
dispatch({
type: "query_error",
error: jsonRes.Error
})
}
})
}
const { movies, loading, errorMessage } = state;
return (
<div className="App">
<Header text="Movie App" />
<Search search={search} />
<p className="App-intro">分享快樂,從電影開始</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
和 useReducer
來看,邏輯會更加的清晰,還會有一些性能上的優化.
6. 總結
基本上這個小小的 demo 到這裏就結束了.
主要就是一些簡單 hooks 的使用以及與 class component
使用的一些代碼上的區別.更多的 hooks 以及自定義 hooks 沒有涉及到,這個在 官方文檔 寫的很詳細,建議大家閱讀.這裏只是借花獻佛的"拋磚引玉"一下.