React Hooks 项目 | 电影搜索网站

971 阅读3分钟

组件构成

  • App.js — 其他 3 个组件的父组件:包含处理 API 请求的函数,包含组件初始呈现期间调用 API 的函数。
  • Header.js — 呈现应用标题并接受标题属性。
  • Movie.js — 渲染每部电影。
  • Search.js — 包含一个带有 input 元素和搜索按钮的表单,包含处理 input 元素和重置字段的函数,还包含一个调用 search 函数的函数,该函数作为 props 传递给它。

步骤

1.设置React应用程序

npx create-react-app movie-search

2.设置组件

1)在src下创建一个文件夹components

包含“组件构成”中所述四个组件。

2)在index.js中更新导入

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './components/App'; // this changed

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

3)设置Header.js并导出

在Header.js中添加如下代码,创建一个用text属性渲染header标签的组件。

import React from "react";

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

export default Header;

4)更新App.css

以下代码包含项目全部样式

.App {
  text-align: center;
}

/*标题样式 */
.App-header {
  /* flex布局 */
  background-color: #282c34;
  height: 70px;
  display: flex;
  flex-direction: column;
  justify-self: center;
  align-items: center;
  /* vmin:当前视窗宽度和高度的百分比中较小的那个,1vw 代表视窗的宽度为 1%
  使文字大小在横竖屏时保持一致*/
  font-size: calc(10px + 2vmin);
  color: white;
  padding: 20px;
  cursor: pointer;
}

.loading {
  display: block;
  width: 20%;
  margin: auto;
}

.App-intro {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
    Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
  font-size: calc(10px + 2vmin);
}

/* new css for movie component */
* {
  /*  width 和  height 属性包括内容,内边距和边框,但不包括外边距 */
  box-sizing: border-box;
}

.movies {
  display: flex;
  flex-wrap: wrap;
  flex-direction: row;
}

.movies h2 {
  display: inline-block;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
    Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
  height: 45px;
  width: 200px;
  font-size: medium;
}

.App-header h2 {
  margin: 0;
}

.movie {
  padding: 5px 25px 10px 25px;
  /* max-width 会覆盖width设置, 但 min-width设置会覆盖 max-width */
  /* 以父级块级容器宽度的百分比<percentage>作为最大宽度. */
  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: lightskyblue;
}

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

5) 设置Movie.js组件并导出

呈现电影标题、图像和年份的展示组件,没有任何内部状态。

设置DEFAULT_PLACEHOLDER_IMAGE以替从API检索到的、没有图像的电影,提供占位符图像。

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;

6) 设置Search.js组件(有状态的)并导出

使用useState API,它让我们可以将 React 状态添加到函数组件中。它接收初始状态作为参数,返回一个数组,数组中包含当前状态、用以更新状态的函数。

将input字段作为当前状态传入。状态改变即onChange事件发生时,handleSearchInputChanges函数被调用,它将调用useState返回的用以更新状态的函数,将状态值更新为当前状态。

然后调用resetInputField函数,它将调用useState返回的用以更新状态的函数,将状态值更新为空字符串,从而实现清除输入字段的目的。

import React, { useState } from 'react';

const Search = ({ search }) => {
    //input输入的状态值
    const [searchValue, setSearchValue] = useState('');

    //更新状态值为input输入的当前状态
    const handleSearchInputChanges = (e) => {
        setSearchValue(e.target.value);
    }
    //重置字段
    const resetInputField = () => {
        setSearchValue('');
    }

    //包含调用search函数的函数,该函数作为props传递给它
    const callSearchFunction = (e) => {
        e.preventDefault();
        search(searchValue);
        resetInputField();
    }

    //包含一个带有input元素、搜索button的表单
    return (
        <form className="search">
            <input
                onChange={handleSearchInputChanges}
                type="text"
                value={searchValue}
            />
            <input
                onClick={callSearchFunction}
                type="submit"
                value="SEARCH"
            />
        </form>
    );
}

export default Search;

7)更新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=princess&apikey=4a3b711b";

定义App函数:

设置三个useState函数,以处理加载状态、从服务器获取电影数组、处理API请求可能遇到的错误。

使用useEffect函数,在组件挂载时从MOVIE_API_URL获取一次数据。

定义search函数,每次搜索新字段时,更新状态值,并传入搜索行为输入的新字段searchValue,以使用fetch发起HTTP请求。如下:

const App = () => {
    //处理加载状态
    const [loading, setLoading] = useState(true);
    //从服务器获取电影数组
    const [movies, setMovies] = useState([]);
    //处理在发出API请求时可能发生的任何错误
    const [errorMessage, setErrorMessage] = useState(null);

    //在组件初始呈现期间调用API
    //使用useEffect函数隔离副作用
    useEffect(() => {
        fetch(MOVIE_API_URL)
            .then(res => res.json())
            .then(result => {
                setMovies(result.Search);
                setLoading(false);
            });
    }, []);

    const search = searchValue => {
        setLoading(true);
        setErrorMessage(null);

        //处理API请求
        fetch(`https://www.omdbapi.com/?s=${searchValue}&apikey=4a3b711b`)
            .then(res => res.json())
            .then(result => {
                if (result.Response === "True") {
                    setMovies(result.Search);
                    setLoading(false);
                } else {
                    setErrorMessage(result.error);
                    setLoading(false);
                }
            });
    };

    const { movies, errorMessage, loading } = state;

    return (
        <div className="App">
            <div className="m-container">
                <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>
        </div>
    );
};

【优化】可以将获取展示电影数组封装进一个函数。如下:

 const retrivedMovies =
        loading && !errorMessage ? (
            <span>loading</span>
        ) : errorMessage ? (
            <div className="errorMessage">{errorMessage}</div>
        ) : (
            movies.map((movie, index) => (
                <Movie key={`${index}-${movie.Title}`} movie={movie} />
            ))
        )

    return (
        <div className="App">
            <div className="m-container">
                <Header text="HOOKED" />
                <Search search={search} />
                <p className="App-intro">Sharing a few of our favourite movies</p>
                <div className="movies">{retrivedMovies}</div>
            </div>
        </div>
    );

【优化】由于我们使用了三个相互关联的useState函数,可以考虑改为使用useReducer函数。axios中数据的字符串化是自动完成的,可以考虑将获取数据的方式由fetch改为axios。

添加gif加载图片。

import { initialState, reducer } from '../store/reducer';
import spinner from '../image/giphy.gif';
import axios from 'axios';
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(() => {
        axios.get(MOVIE_API_URL).then(result => {
            dispatch({
                type: "SEARCH_MOVIES_SUCCESS",
                payload: result.data.Search
            });
        });
    }, []);

    const search = searchValue => {
        dispatch({
            type: "SEARCH_MOVIES_REQUEST"
        });

        axios(`https://www.omdbapi.com/?s=${searchValue}&apikey=4a3b711b`)
            .then(result => {
                if (result.data.Response === "True") {
                    dispatch({
                        type: "SEARCH_MOVIES_SUCCESS",
                        payload: result.data.Search
                    });
                } else {
                    dispatch({
                        type: "SEARCH_MOVIES_FAILURE",
                        error: result.data.Error
                    });
                }
            });
    };

    const { movies, errorMessage, loading } = state;

    const retrivedMovies =
        loading && !errorMessage ? (
            <img className="loading" src={spinner} alt="Loading spinner" />
        ) : errorMessage ? (
            <div className="errorMessage">{errorMessage}</div>
        ) : (
            movies.map((movie, index) => (
                <Movie key={`${index}-${movie.Title}`} movie={movie} />
            ))
        )

    return (
        <div className="App">
            <div className="m-container">
                <Header text="HOOKED" />
                <Search search={search} />
                <p className="App-intro">Sharing a few of our favourite movies</p>
                <div className="movies">{retrivedMovies}</div>
            </div>
        </div>
    );
};

export default App;

8) 封装reducer函数

在src下新建store文件夹,在其中新建reducer文件夹,然后新建index.js文件。

将如下代码剪切到文件中并导出:

export const initialState = {
    loading: true,
    movies: [],
    errorMessage: null
};

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

原文链接:How to build a movie search app using React Hooks