最终完成效果
初始化项目
hooked是APP名字
1 npm install -g create-react-app
如果没有安装create-react-app,请输入这段
完成后,我们应该有一个名为“ Hooked”的文件夹,其目录结构如下所示:
在此应用程序中,我们将有4个组件,因此让我们概述每个组件及其功能:
App.js —它将是其他3的父组件。它还将包含处理API请求的函数,并且具有在组件的初始呈现期间调用API的函数。
Header.js —一个简单的组件,可呈现应用程序标题并接受标题道具
Movie.js —渲染每部电影。电影对象只是作为道具传递给它的。
Search.js —包含带有输入元素和搜索按钮的表单,包含处理输入元素并重置字段的函数,还包含调用作为道具传递给它的搜索函数的函数。
让我们开始在src目录中创建一个新文件夹并将其命名,components因为这是我们所有组件所在的位置。然后,我们将App.js文件移动到该文件夹中。然后,我们将创建Header组件。创建一个名为的文件,Header.js并向其中添加以下代码:
1 2 3 4 5 6 7 8 9 10 11 import React from "react" ;const Header = (props ) => { return ( <header className="App-header" > <h2>{props.text}</h2> </header> ); }; export default Header;
这个组件不需要太多解释-它基本上是一个功能组件,header使用text道具呈现标签。让我们不要忘记更新index.js文件中的导入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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();
样式 并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 109 110 111 112 113 114 .App { text-align: center; } .App-header { background-color: #282c34; height: 70 px; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: calc(10 px + 2 vmin); color: white; padding: 20 px; cursor: pointer; } .spinner { height: 80 px; 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: 16 px; padding: 8 px; margin: 0 10 px 30 px 10 px; } .movie { padding: 5 px 25 px 10 px 25 px; 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: 10 px; } input[type="submit" ] { padding: 5 px; background-color: transparent; color: black; border: 1 px solid black; width: 80 px; margin-left: 5 px; cursor: pointer; } input[type="submit" ]:hover { background-color: #282c34; color: antiquewhite; } .search > input[type="text" ]{ width: 40 %; min-width: 170 px; } @media screen and (min-width: 694 px) and (max-width: 915 px) { .movie { max-width: 33 %; } } @media screen and (min-width: 652 px) and (max-width: 693 px) { .movie { max-width: 50 %; } } @media screen and (max-width: 651 px) { .movie { max-width: 100 %; margin: auto; } }
一旦有了这些,下一步就是创建Movie组件。我们将通过创建一个名为的文件Movie.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 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;
这需要更多的解释,但它只是呈现电影标题,图像和年份的表示性组件(没有任何内部状态)。这样做的原因DEFAULT_PLACEHOLDER_IMAGE是因为从API检索的某些电影没有图像,因此我们将呈现一个占位符图像而不是断开的链接。
现在,我们将创建Search组件。这部分令人兴奋,因为在过去,为了处理内部状态,我们将不得不创建一个类组件……但现在不再了!因为使用钩子,我们可以使功能组件处理其自身的内部状态。让我们创建一个名为的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 27 28 29 30 31 32 33 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 value={searchValue} onChange={handleSearchInputChanges} type="text" /> <input onClick={callSearchFunction} type="submit" value="SEARCH" /> </form> ); } export default Search;
这太令人兴奋了!!!我确定您已经看到了我们将要使用的第一个hooks API,它被称为useState。顾名思义,它使我们可以将React状态添加到功能组件中。所述useState钩接受一个参数,它是在初始状态,然后它返回一个包含当前的状态(相当于一个数组this.state为类组件)和一个函数进行更新(相当于this.setState)。
在本例中,我们将当前状态作为搜索输入字段的值。调用onChange事件时,将handleSearchInputChanges调用该函数,该函数将使用新值调用状态更新函数。该resetInputField函数基本上setSearchValue用空字符串调用状态更新函数(),以清除输入字段。查看此内容以了解有关useStateAPI的更多信息。
App.js 最后,我们将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 59 60 61 62 63 64 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 = "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="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;
让我们看一下代码:我们正在使用3个useState函数,所以是的,我们可以useState在一个组件中拥有多个函数。第一个用于处理加载状态(将loading设置为true时,它将呈现“ loading…”文本)。第二个用于处理从服务器获取的电影数组。最后,第三个用于处理发出API请求时可能发生的任何错误。
在那之后,我们遇到了我们在应用程序中使用的第二个钩子API:useEffect钩子。该钩子基本上使您可以在功能组件中执行副作用。所谓副作用,是指诸如数据获取,订阅和手动DOM操作之类的事情。关于这个钩子的最好的部分是来自React官方文档的引言:
如果你熟悉阵营类生命周期方法,你能想到的useEffect钩。因为componentDidMount,componentDidUpdate和componentWillUnmount结合。 这是因为useEffect在第一个渲染(componentDidMount)之后以及每次更新(componentDidUpdate)之后都会被调用。
我知道您可能想知道这与componentDidMount每次更新后都调用它有何相似之处。好吧,这是因为该useEffect函数接受两个参数,一个是您要运行的函数,另一个是数组。在该数组中,我们只是传入一个值,该值告诉React如果传入的值未更改,则跳过应用效果。
根据文档,这类似于我们在条件中添加条件语句时的情况componentDidUpdate:
1 2 3 4 5 6 7 8 9 10 11 12 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这个效果应该被调用一次。
如您所见,我们有3个useState功能有些相关,应该可以将它们以某种方式进行组合。值得庆幸的是,React团队为我们提供了服务,因为他们制作了一个有助于此操作的钩子-将该钩子称为useReducer。让我们将App组件转换为使用新的钩子,这样我们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 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 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挂钩的工作原理。
该挂钩具有3个参数,但在我们的用例中,我们将仅使用2个。典型的useReducer挂钩如下所示:
1 2 3 4 const [state, dispatch] = useReducer( reducer, initialState );
该reducer参数类似于我们在Redux中使用的参数,如下所示: Redux中使用的参数
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 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; } };
精简器接受initialState和操作,因此精简器根据操作类型返回一个新的状态对象。例如,如果调度的操作类型为SEARCH_MOVIES_REQUEST,则状态将使用新对象更新,其中for的loading值为true,并且errorMessage为null。
要注意的另一件事是,在我们的中useEffect,我们现在正在调度一个带有有效负载的操作,作为从服务器获取的电影数组。此外,在我们的search职能中,我们实际上是在分派三个不同的动作。
一种动作是SEARCH_MOVIES_REQUEST更新我们的状态对象make 的动作loading=true and errorMessage = null。 如果请求成功,那么我们将分派另一个操作,该操作的类型SEARCH_MOVIES_SUCCESS 将更新状态对象,从而loading=false and movies = action.payload使有效负载是从OMDB获取的电影数组。 如果有错误,我们反而会派遣与类型不同的操作SEARCH_MOVIES_FAILURE,更新我们的状态对象制作loading=false and errorMessage = action.error,其中action.error从服务器得到该错误消息。 要了解有关useReducer钩子的更多信息,请查看官方文档。
这是本文的GitHub存储库的链接。
github