[译] 使用 React Hooks 构建电影搜索应用程序

前言:

原文地址:How to build a movie search app using React Hooksjavascript

在这篇文章中,咱们将使用 React Hooks 构建一个很是简单的应用程序。所以,咱们不会在此应用程序中使用任何class 组件。 我将解释一些API的工做原理,以便于使你能在构建其它应用程序时能更驾轻就熟地使用 React Hooks。css

如下是完成这个应用程序以后的页面截图:html

image.png

我知道,这名字看起来颇有创造性...

基本上,该程序可经过  OMDB API 来搜索电影并将结果返回给咱们。构建此应用程序的目的在于使咱们更加理解 React Hooks 而且助你在本身开发的项目中更好地使用它,那么,咱们开始吧!在此以前,你须要作一些事情:

  • Node (>=6)
  • 有一个超酷的代码编辑器 (我用的是 vscode)
  • OMDB的API key (你能够在此处获取或使用个人)

开始构建

建立 React app

这个教程将会使用 react 脚手架工具 create-react-app 来构建咱们的应用,若是你尚未安装这个脚手架工具,在终端执行如下命令:java

npm install -g create-react-app
复制代码

接下来,建立咱们的 React app,在终端输入如下命令:react

create-react-app hooked
复制代码

"hooked" 是咱们建立的 app 的名字git

完成后,咱们应该有一个名为 “Hooked” 的文件夹,其目录结构以下所示:github

image.png

初始化的项目结构

建立所需组件

此应用程序中包含4个组件,我来概述下每一个组件及其功能:npm

  • App.js  — 它将是其余3个组件的父组件。它还将包含处理 API 请求的函数,而且具备在组件的初始渲染期间调用API的函数。
  • Header.js  — 一个简单的组件,可呈现应用程序标题并接收标题 prop
  • Movie.js  — 它渲染每一个 movie 。 movie 对象做为 props 传递给它。
  • Search.js  — 包含具备输入元素和搜索按钮的表单,包含处理输入元素并重置字段的函数,还包含调用做为 props 传递给它的搜索函数的函数。

让咱们开始建立它们吧,在 src 目录下,建立一个新文件夹命名为 components ,这个文件夹存放咱们全部的组件,将 App.js 文件拖进去。接着,咱们建立一个新的文件命名为 Header.js ,并输入如下代码:json

import React from 'react';

const Header = (props) => {
  return (
    <header className="App-header"> <h2>{props.text}</h2> </header>
  )
}

export default Header;
复制代码

这个组件不须要太多的解释,就是一个很基本的组件,接受 props ,并将 props.text 渲染为页面标题。api

别忘记更新咱们的 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')); // 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: https://bit.ly/CRA-PWA serviceWorker.unregister(); 复制代码

如今你执行 npm run start 必然是成功不了的,一是咱们 App.js 路径变了,引入 App.css 的路径也没改,并且许多默认构建的元素如 logo.svg 路径都没变,咱们如今先不急,等咱们把组件都写好以后,在集中修改 App.js 。

接下来在 components 下继续建立一个新的组件 Movie.js ,添加如下代码:

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.js ,这部分很使人激动,由于在过去,为了处理内部状态,咱们不得不建立一个 class 组件... 可是如今不用了!由于有了 hooks ,咱们如今能够建立一个普通的函数就能处理内部状态,就问你厉不厉害。在文件夹 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) => {
    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;
复制代码

这真的太酷了,你不用像之前同样在 class 组件中的 constructor 中建立状态,利用 setState 更新状态,以及繁琐的 .bind(this) 。我相信你已经看过了咱们使用的 useState ,顾名思义,它使咱们能够将 React 状态添加到普通函数组件中。 useState 接受一个参数,该参数是初始状态,而后返回一个包含当前状态(等同于类组件的 this.state )和更新它的函数(等同于 this.setState )的数组。

在本例中,咱们将当前状态做为搜索输入字段的值。 由于注册了 onChange 事件,在输入改变时,将调用 handleSearchInputChanges 函数,该函数使用新的输入值去更新当前状态。 resetInputField 函数就是重置输入框的值为空字符串。 点我了解更多 useState API 信息。

最后,咱们来解决咱们以前留下的坑,更新 App.js :

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">分享一些为喜欢的电影</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 就是一个 Effect Hook,给函数组件增长了操做反作用的能力。它跟 class 组件中的 componentDidMountcomponentDidUpdate 和 componentWillUnmount 具备相同的用途,只不过被合并成了一个 API。

其实就是说, useEffect 在首次渲染(componentDidMount)以及以后每次更新(componentDidUpdate)都被调用。

我知道你可能想知道若是每次更新后都调用它,那与 componentDidMount 有何类似之处呢? Emmm..,这是由于 useEffect 函数接受两个参数,一个是你要运行的函数,另外一个是数组,你仔细看看官方文档的代码或上面咱们本身写的代码。 在该数组中,咱们只传入一个值,该值告诉 React 若是传入的值没有被更改,则跳过这次调用。

根据文档,这相似于咱们在 componentDidUpdate 中添加条件语句时的状况:

// class 组件
componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}


// 使用 hooks 的函数组件
useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 只有 count 值变了,才会从新执行
复制代码

在咱们的例子中,咱们没有任何变化的值,所以咱们能够传入一个空数组,该数组告诉 React 这个效果应该被调用一次。

如你所见,咱们有3个 useState 函数,它们看起来有相关性,应该有可能将它们组合在一块儿。 为了作到这点,React 团队已经为咱们想到了,因而他们制做了一个有助于此操做的钩子 - 该钩子称为 useReducer 。 让咱们将App组件转换为使用 useReducer 的新组件,这样咱们的 App.js 如今将以下所示:

import React, { useEffect, useReducer } 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">分享一些为喜欢的电影</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 挂钩的工做方式。

该 hook 接受3个参数,但在咱们的用例中,咱们将仅使用2个。典型的 useReducer 钩子以下所示:

const [state, dispatch] = useReducer(
  reducer,
  initialState
);
复制代码

reducer 参数相似于咱们在 Redux 中使用的参数,看起来像这样:

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;
  }
}
复制代码

reducer 接收 initialState 和 action ,所以 reducer 根据 action.type 返回一个新的状态对象。 例如,若是调度的操做类型为 SEARCH_MOVIES_REQUEST ,则状态将使用新对象更新,其中 loading 的值为 true ,而 errorMessage 为 null。

值得一提的是,在搜索功能中,咱们其实是在分派三个不一样的动做:

  • 一个动做是 SEARCH_MOVIES_REQUEST 动做,它更新咱们的状态对象,使 loading = true 且 errorMessage = null 。
  • 若是请求成功,那么咱们将分派另外一个类型为 SEARCH_MOVIES_SUCCESS 的动做,该动做将更新咱们的状态对象,从而使 loading = false 和 movie = action.payload ,其中 payload 是从OMDB得到的movie 数组。
  • 若是出现错误,咱们将分派类型为 SEARCH_MOVIES_FAILURE 的其余操做,该操做更新状态对象,使 loading = false 和 errorMessage = action.error ,其中 action.error 是从服务器获取的错误消息。

要了解有关 useReducer 钩子的更多信息,请查看官方文档

最后修改咱们的 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;
}

.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;
  }
}
复制代码

你作到了!

哇!!! 咱们已经走了很长一段路,我相信你对 hooks 的可能性感到兴奋。 就我我的而言,将初学者介绍给 React 很是容易,由于我再也不须要解释 class 的工做方式或 this 的工做方式,或者在JS中 bind 的工做方式。

在本教程中,咱们仅涉及了一些钩子,甚至没有介绍建立本身的自定义钩子等功能。 若是您还有其余用钩的用例,或者已经实现了本身的自定义钩,请添加评论并加入其中。

这篇文章的代码就不提供了,我但愿任何能安心看下来的小伙伴能手动敲一边,收获仍是有的!~

后记:

由于笔者也是刚刚学 React hooks,在掘金上另外一篇文章中看到了推荐这个项目,本身读了一遍,作了一遍,发现做为入门仍是不错的,故想翻译一下让更多学习 React hooks 的小伙伴能学习到~如果翻译有误,还请指正,谢谢啦🙏。

这篇文章收录于我本身的Github/blog,若对你有所帮助,欢迎 star,以后会陆续推出更多基础优质文章~

相关文章
相关标签/搜索