[譯] 使用 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,以後會陸續推出更多基礎優質文章~

相關文章
相關標籤/搜索