React 電影demo - 使用 hooks(Class component VS Functional component)

基於電影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 發生了變化:
添加 Movie 組件

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;

對比使用 useStateuseReducer 來看,邏輯會更加的清晰,還會有一些性能上的優化.

6. 總結

基本上這個小小的 demo 到這裏就結束了.
主要就是一些簡單 hooks 的使用以及與 class component使用的一些代碼上的區別.更多的 hooks 以及自定義 hooks 沒有涉及到,這個在 官方文檔 寫的很詳細,建議大家閱讀.這裏只是借花獻佛的"拋磚引玉"一下.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章