components

  • App.js – The parent of the other three components: contains functions that handle API requests, and contains functions that call the API during the initial rendering of the component.
  • Header.js – Renders the application title and accepts the title property.
  • Movie.js – Render each Movie.
  • Search.js – contains a form with an input element and a Search button, a function that handles the input element and resets the field, and a function that calls the Search function, passed to it as props.

steps

1. Configure the React application

npx create-react-app movie-search
Copy the code

2. Set components

1) Create a folder “components” under SRC

Contains the four components described in Component Composition.

2) Update the import in 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'));
Copy the code

3) Set header. js and export it

Add the following code to header. js to create a component that renders the Header tag with the text property.

import React from "react";

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

export default Header;
Copy the code

4) Update app.css

The following code contains all of the project styles

.App {
  text-align: center;
}

/* Title style */
.App-header {
  /* Flex layout */
  background-color: #282c34;
  height: 70px;
  display: flex;
  flex-direction: column;
  justify-self: center;
  align-items: center;
  /* vmin: the smaller percentage of the current window width and height. 1vw means that the window width is 1% so that the text size is the same across the screen */
  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 and height attributes include content, inner margins, and borders, but not margins */
  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 overwrites width, but min-width overwrites max-width */
  /* The maximum width of the parent block-level container is 
      
       . */
      
  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; }}Copy the code

5) Set up the movie.js component and export it

A presentation component that renders the movie title, image, and year, without any internal state.

Set DEFAULT_PLACEHOLDER_IMAGE to provide placeholder images for movies retrieved from the API that have no images.

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:The ${movie.Title} `}src={poster}
        />
      </div>
      <p>({movie.Year})</p>
    </div>
  );
};


export default Movie;
Copy the code

6) Set up the search.js component (stateful) and export it

Using the useState API, it lets you add React state to function components. It takes the initial state as an argument and returns an array containing the current state and a function to update the state.

The INPUT field is passed in as the current state. When the onChange event occurs, the handleSearchInputChanges function is called, which calls the function returned by useState to update the status value to the current state.

The resetInputField function is then called, which calls the function returned by useState to update the status value to an empty string, thus clearing the input field.

import React, { useState } from 'react';

const Search = ({ search }) = > {
    //input Indicates the status of the input
    const [searchValue, setSearchValue] = useState(' ');

    // Update the status value to the current state of the input
    const handleSearchInputChanges = (e) = > {
        setSearchValue(e.target.value);
    }
    // Reset the field
    const resetInputField = () = > {
        setSearchValue(' ');
    }

    // contains the function that calls the search function, which is passed to it as props
    const callSearchFunction = (e) = > {
        e.preventDefault();
        search(searchValue);
        resetInputField();
    }

    // contains a form with input elements that search for button
    return (
        <form className="search">
            <input
                onChange={handleSearchInputChanges}
                type="text"
                value={searchValue}
            />
            <input
                onClick={callSearchFunction}
                type="submit"
                value="SEARCH"
            />
        </form>
    );
}

export default Search;
Copy the code

7) Update the app.js component

Pass in the style and three child components

import React, { useState, useEffect } from 'react';
import '.. /App.css';
import Header from './Header';
import Movie from './Movie';
import Search from './Search';
Copy the code

Sets the array of movies displayed on the initial page.

const MOVIE_API_URL = "https://www.omdbapi.com/?s=princess&apikey=4a3b711b";
Copy the code

Define App functions:

Set up the three useState functions to handle the load state, get the movie array from the server, and handle errors that may be encountered with API requests.

Use the useEffect function to get data once from MOVIE_API_URL while the component is mounted.

Define the search function that updates the status value each time a new field is searched and passes in the new field searchValue entered by the search behavior to initiate an HTTP request using FETCH. As follows:

const App = () = > {
    // Handle the load state
    const [loading, setLoading] = useState(true);
    // Get the movie array from the server
    const [movies, setMovies] = useState([]);
    // Handle any errors that may occur while making API requests
    const [errorMessage, setErrorMessage] = useState(null);

    // Call the API during the initial rendering of the component
    Use the useEffect function to isolate side effects
    useEffect(() = > {
        fetch(MOVIE_API_URL)
            .then(res= > res.json())
            .then(result= > {
                setMovies(result.Search);
                setLoading(false); }); } []);const search = searchValue= > {
        setLoading(true);
        setErrorMessage(null);

        // Process API requests
        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}-The ${movie.Title} `}movie={movie} />)))}</div>
            </div>
        </div>
    );
};
Copy the code

[Optimization] Can get show movie array wrapped in a function. As follows:

 constretrivedMovies = loading && ! errorMessage ? (<span>loading</span>
        ) : errorMessage ? (
            <div className="errorMessage">{errorMessage}</div>
        ) : (
            movies.map((movie, index) = > (
                <Movie key={` ${index}-The ${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>
    );
Copy the code

Since we use three useState functions that are related to each other, we can consider using the useReducer function instead. Stringing of data in AXIos is done automatically, so consider changing the way data is fetched from FETCH to Axios.

Add giFs to load images.

import { initialState, reducer } from '.. /store/reducer';
import spinner from '.. /image/giphy.gif';
import axios from 'axios';
Copy the code
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:
            returnstate; }};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;

    constretrivedMovies = loading && ! errorMessage ? (<img className="loading" src={spinner} alt="Loading spinner" />
        ) : errorMessage ? (
            <div className="errorMessage">{errorMessage}</div>
        ) : (
            movies.map((movie, index) = > (
                <Movie key={` ${index}-The ${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;
Copy the code

8) Encapsulate the Reducer function

Create a Store folder under SRC, create a Reducer folder, and create the index.js file.

Cut the following code into a file and export it:

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:
            returnstate; }};Copy the code

How to build a movie Search app using React Hooks