这个 App 实现的效果是通过OMDB API来搜索电影并将结果展现给用户。搭建这个 App 的过程能帮助我们很好的学习React Hooks的用法,练习实际项目能帮助更快上手。
App 完成后的效果预览
项目用到的工具
- Node
- VsCode
- OMDB 的 API key (这里获得)
准备好上述工具后,我们需要用 react 的脚手架工具来帮助我们搭建一个全新的 React 应用程序,安装create-react-app脚手架:
1 2
| npm i -g create-react-app //或者用cnpm yarn都可以
|
然后通过脚手架新建项目:
1
| create-react-app movie-search-app
|
完成之后用 VsCode 打开该目录,目录结构如下图所示:
构成该 App 的 4 个组件
- App.js —— 它是其它 3 个组件的父组件,将包含处理 API 请求的函数以及组件初始化时调用的 API
- Header.js —— 接受参数并展示 App 的标题
- Movie.js —— 渲染每个电影,电影对象将通过参数传递给它
- Search.js —— 包含一个带有输入和搜索按钮的表单,处理输入和重置的函数以及一个作为参数传递给它的搜索函数
开始着手构建我们的 APP
在src
目录下新建一个文件夹命名为components
(之后所有组件都将保存在这个地方),然后把App.js
组件移动到该目录下。新建一个Header.js
组件用于展示程序的标题,并加入如下代码:
1 2 3 4 5 6 7 8 9 10 11 12
| import React from "react";
const Header = (props) => { return ( <header className="App-header"> <h2>{props.text}</h2> </header> ); };
export default Header;
|
更新src
目录下index.js
中的导入
1 2
| import App from './components/App';
|
并且更新App.css
中的样式代码(供参考):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
| .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; }
* { 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; } }
|
随时可以用npm start启动项目来查看效果
下一步创建Movie.js
组件,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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 src={poster} alt={`The movie titled:${movie.Title}`} width="200" /> </div> <p>({movie.Year})</p>//展示电影年份 </div> ) }
export default Movie;
|
接下来开始创建Search
组件。这是最关键的一部分,因为使用Hooks之前的React为了处理内部状态需要创建一个类组件…不过现在利用Hooks可以在函数组件内部处理自己的状态。在components
文件夹下新建Search.js
文件并加入如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| 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) => { e.preventDefault(); props.search(searchValue); resetInputField(); }
return( <form className="search"> <input type="text" value={searchValue} onChange={handleSearchInputChanges}/> <input type="submit" value="SEARCH" onClick={callSearchFunction} /> </form> ) }
export default Search;
|
Hooks API - useState介绍
在Search.js
组件中使用了一个hooks API,即useState。顾名思义它允许我们向函数组件添加React状态。useState钩子接收一个初始状态参数,并返回一个数组包含当前的状态(this.state)和一个更新它的函数(this.setState)。
在我们的示例中,我们将当前状态作为搜索输入值传递。在输入框触发onChange事件时,将调用handleSearchInputChanges函数用于更新输入的搜索值。resetInputField方法用于清空当前搜索框的值。(更多useState内容)
接下来让我们更新App.js
中的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| import React, { useEffect, useState } 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 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); }); },[]);
const search = searchValue => { setLoading(true); setErrorMessage(null)
fetch(`https://www.omdbapi.com/?s=${searchValue}&apikey=4a3b711b`) .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="MovieSearchApp"></Header> <Search 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;
|
在上述代码中,我们用到了3个useState函数,第一个函数用于处理当前加载状态;第二个函数用于处理从服务器获取的电影数组;第三个函数用于处理API请求时可能返回的错误信息。
然后我们用到了第二种hooks API:useEffect
这个钩子基本上能让你在函数组件中执行副作用,副作用指的是例如数据获取,订阅和手动操作DOM这类事情。这个钩子最棒的一部分来自官方文档的介绍:
如果你熟悉React的类的生命周期方法,你可以将useEffect看作componentDidMout,componentDidUpdate和componentWillUnmount的结合。
这是因为useEffect会在第一次渲染(componentDidMount)之后和每次更新(componentDidUpdate)之后进行调用。
你可能想知道如果它在每次更新之后都进行调用,那么它和componentDidMount有何相似的地方?那是因为这个函数接受两个参数,一个是你想要运行的函数另一个是数组。在该数组中我们只需要传入一个值去告诉React如果该值没有修改则跳过应用的函数效果。
根据文档,这和我们在componentDidUpdate中添加一个if判断语句类似:
1 2 3 4 5 6 7 8 9 10 11
| componentDidUpdate(prevProps, prevState) { if(prevState.count !== this.state.count){ document.title = `You clicked ${this.state.count} times`; } }
useEffect(() => { document.title = `You clicked ${count} times`; },[count]);
|
在我们的代码中初始化时并没有要改变的值,所以可以传入一个空数组来告诉React这个方法需要调用一次。
如你所见,我们有三个存在一定联系的useState函数,所以它们应该可以用某种方式组合起来。因此React为我们提供了另一个hook——useReducer。利用这个钩子我们的代码变成如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| 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, movies: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="MovieSearchApp"></Header> <Search 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> ); };
|
useReducer钩子接受3个参数,不过我们只使用了其中2个。一个典型的用力如下:
1
| const [state, dispatch] = useReducer(reducer,initialState);
|
在我们的代码中,reducer接收我们定义的initialState对象和一系列操作,基于操作的类型返回给我们新的状态对象。例如我们的操作类型是”SEARCH_MOVIES_REQUEST“,状态将更新为”loading=true,errorMessage=null”。
另一件需要注意的事情是,在useEffect中,我们将从服务器获取的电影数组作为payload来执行dispatch操作,在search
方法中我们实际上有3个不同的操作。
- SEARCH_MOVIES_REQUEST:更新状态对象,loading=true,errorMessage=false。
- SEARCH_MOVIES_SUCCESS:如果请求成功,那么更新状态,loading=false,movies=a_ction.payload。payload是从OMDB获取到的电影搜索结果。
- SEARCH_MOVIES_FAILURE:请求失败的话,loading=false,errorMessage=action.error。error也是服务器返回的错误消息。
总结
至此我们整个项目做完了,用到了useState、useReducer、useEffect三个hooks。对React hooks的用法有了一个基本的了解,更多详细的内容推荐去官网阅读。
原文链接:
https://www.freecodecamp.org/news/how-to-build-a-movie-search-app-using-react-hooks-24eb72ddfaf7/