最终完成效果

初始化项目

1
create-react-app hooked

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'; // this changed
import * as serviceWorker from './serviceWorker';


ReactDOM.render(<App />, document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA


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: 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;
}
}

一旦有了这些,下一步就是创建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"; // you should replace this with yours


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
// for class components
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}


// using hooks it will become
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes

在我们的例子中,我们没有任何变化的值,因此我们可以传入一个空数组,该数组告诉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