Artigo original: https://www.freecodecamp.org/news/how-to-build-a-movie-search-app-using-react-hooks-24eb72ddfaf7/

O React Hooks chegou há 3 anos. Houve muita empolgação em torno das possibilidades que esse conjunto de APIs apresentava, e ainda há. Porém, caso você ainda esteja cético em relação a eles, recomendo que você verifique este artigo (em inglês) para entender os problemas que o time estava tentando resolver quando propôs os hooks.

Eu inicialmente não estava tão empolgado quanto o resto da comunidade, então decidi esperar até que fosse lançado oficialmente, caso houvesse alguma mudança na API. No fim de semana após o lançamento, eu decidi ler sobre eles e, surpreendentemente, (não) havia muitos artigos e posts sobre hooks e como começar com eles.

Eu sei que alguns podem dizer "Outro artigo sobre hooks, sério?", e, para essas pessoas, eu digo "Sim… sim, e há mais de onde veio esse". Para este artigo, construiremos um aplicativo muito simples usando os hooks. Em essência, não usaremos nenhum componente de classe neste aplicativo. Explicarei também como algumas das APIs funcionam e como elas devem ser usadas em qualquer aplicativo que estejamos construindo.

Abaixo está uma imagem de como o aplicativo ficará assim que terminarmos:

kbYsxsxb2D7mBhdlEmUrpMhRmOcQoR79vtT1
O nome do app é supercriativo, eu sei!

Basicamente, o aplicativo poderá pesquisar filmes por meio da API da OMDB e renderizar os resultados para o usuário. A razão para construir o aplicativo é apenas para que possamos entender melhor o uso de hooks em um aplicativo, o que ajuda a entender o papel que alguns dos hooks que usaremos podem desempenhar em seus próprios aplicativos do mundo real. Algumas coisas são necessárias antes de começarmos a construir o aplicativo:

  • Node (com versão ≥ 6)
  • Uma IDE ou um editor de texto legal
  • Uma chave de API do OMDB (você pode obtê-la aqui ou usar a minha)

Ótimo. Assim que tivermos essas coisas, o próximo passo é configurar o aplicativo do React. Para este tutorial, usaremos o create-react-app - ferramenta realmente incrível para configurar um aplicativo em React sem ter que lidar com todas as configurações necessárias se for iniciar do zero. Você pode criar um novo aplicativo digitando:

itZekSefp8FQezvCnNBJJ-HxtGbESGVb-PNt

Se preferir copiar e colar:

create-react-app hooked # "hooked" é o nome do nosso aplicativo

# se você não tem instalado o create-react-app é só digitar o seguinte

npm install -g create-react-app

Feito isso, devemos ter uma pasta chamada "Hooked" com uma estrutura de diretórios conforme a foto abaixo:

XVEd05SM1ul1KYZ-HWoY3-06cFeeyZS1HemM
ESTRUTURA INICIAL DO PROJETO

Teremos 4 componentes nesse aplicativo, então vamos definir cada um e sua funcionalidade:

  • App.js — É o componente de alto nível, que será o pai dos outros 3. Ele também conterá a função que trata da requisição da API e terá uma função que chama a API durante a renderização inicial do componente.
  • Header.js — Um componente simples que renderiza o cabeçalho do aplicativo e aceita uma propriedade de título
  • Movie.js — renderiza cada filme. O objeto de filme é simplesmente passado para ele com propriedades (ou props).
  • Search.js — Contém um formulário com o elemento de entrada(input) e o botão de pesquisa, contém funções que tratam o elemento de entrada e o redefinem, além de conter uma função que chama a função de pesquisa que é passada como props para ela.

Vamos começar criando, na pasta src, uma nova pasta e nomeá-la components, porque é onde estarão todos os nossos componentes. Em seguida, moveremos o arquivo App.js para essa pasta. Depois, vamos criar o componente Header. Agora, criaremos um arquivo chamado Header.js e adicionaremos a ele o código a seguir:

import React from "react";

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

export default Header;

Este componente não precisa de muita explicação — é basicamente um componente funcional que renderiza a tag header com a prop text.

Não esqueça de atualizar a importação do App.js no arquivo index.js:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './components/App'; // isso mudou
import * as serviceWorker from './serviceWorker';


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

// Se quiser que seu aplicativo funcione off-line e carregue mais rápido, 
// mude unregister() para register() abaixo. Há algumas armadilhas em se
// fazer isso. Saiba mais sobre service workers aqui: http://bit.ly/CRA-PWA

serviceWorker.unregister();

Atualize, também, o App.css com esses estilos (não é obrigatório):

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

/* css novo para o componente Movie */

* {
  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;
  }
}

Uma vez que temos isso, a próxima coisa é criar o componente Movie. Faremos isso criando um arquivo chamado Movie.js e adicionando esse código:

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;

Bom, esse também é apenas um componente de apresentação (não possui nenhum state interno) que renderiza o título do filme, a imagem e o ano. A razão para o DEFAULT_PLACEHOLDER_IMAGE é porque alguns filmes recuperados da API não possuem imagens, então renderizaremos uma imagem de espaço reservado em vez de mostrar um link quebrado.

Agora, vamos criar o componente Search. Essa parte é empolgante, porque, no passado, para lidar com o state interno, teríamos que criar um componente de classe… agora, não mais! Com hooks, podemos ter um componente funcional lidando com seu próprio state interno. Vamos criar um arquivo chamado Search.js e, nesse arquivo, adicionaremos:

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;

Isso é emocionante! Tenho certeza de que você acabou de ver a primeira API do hooks que vamos usar, e ela se chama useState. Como o próprio nome indica, ela nos permite adicionar o state do React aos componentes funcionais. O hook useState aceita um argumento que é o state inicial. Em seguida, retorna um array contendo o state atual (equivalente ao this.state dos componentes de classe) e uma função para atualizá-lo (equivalente ao this.setState).

No nosso caso, estamos passando nosso state atual como o valor do campo de entrada de pesquisa. Quando o evento onChange é chamado, ele dispara a função handleSearchInputChanges, que chama a função de atualização do state com o novo valor. Já a função resetInputField, basicamente, chama a função de atualização de state (setSearchValue) com uma string vazia para limpar o campo de entrada. Se quiser saber mais sobre a API useState, clique aqui.

Finalmente, atualizaremos o arquivo App.js com esse código:

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"; // você deve substituir essa chave pela sua


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;

Vamos rever o código: estamos usando 3 funções useState. Então, podemos ter várias funções useState em um componente. O primeiro é usado para lidar com o estado de carregamento (ele renderiza o texto 'loading…' quando o carregamento é definido como true). O segundo é usado para lidar com o array de filmes obtido do servidor. E, finalmente, o terceiro é usado para lidar com quaisquer erros que possam ocorrer ao fazer a requisição da API.

Depois disso, nos deparamos com o segundo tipo de hooks que usaremos no aplicativo: o useEffect. Este hook, basicamente, permite que você execute efeitos colaterais em seus componentes funcionais. Por efeitos colaterais, queremos dizer coisas como busca de dados, assinaturas e manipulações manuais do DOM. A melhor parte sobre este hook é esta citação da documentação oficial do React:

Se você estiver familiarizado com os métodos de ciclo de vida da classe do React, pode pensar no Hook useEffect como o componentDidMount, o componentDidUpdate e o componentWillUnmount combinados.

Isso ocorre porque o useEffect é chamado após a primeira renderização (componentDidMount) e também após cada atualização (componentDidUpdate).

Você pode estar se perguntando como isso é parecido com o componentDidMount se ele é chamado após cada atualização, certo? Bem, é porque a função useEffect aceita dois argumentos, a função que você deseja executar e um segundo argumento, que é um array. Nesse array, passamos apenas o valor que diz ao React para pular a aplicação de um effect se o valor passado não mudar.

De acordo com a documentação, é igual a quando adicionamos uma instrução condicional em nosso componentDidUpdate:


// para componentes de classe
componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}


// usando hooks ele se tornará
useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // Apenas execute novamente o efeito se a contagem for alterada

No nosso caso, não temos nenhum valor que mude. Então, podemos passar um array vazio que diz ao React que esse efeito deve ser chamado uma vez.

Como você vê, temos 3 funções useState que possuem alguma relação entre si, e deve ser possível combiná-las de alguma forma. Felizmente, a equipe do React nos ajudou nisso também, porque criou um hook para isso - e esse hook é chamado useReducer. Assim, vamos converter nosso componente App para usar nosso novo hook. Agora, nosso App.js ficará assim:

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;

Se tudo deu certo, não devemos ver nenhuma mudança no comportamento do aplicativo. Agora, vamos ver como o useReducer funciona.

Esse hook recebe 3 argumentos, mas, para nosso caso de uso, usaremos apenas 2. Um useReducer típico seria mais ou menos assim:

const [state, dispatch] = useReducer(
    reducer,
    initialState
);

O argumento do reducer é semelhante ao que usamos no Redux, assim:

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

O reducer recebe o initialState e a ação, portanto, com base no tipo de ação, o reducer retorna um novo objeto do state. Por exemplo, se o tipo de ação enviada for SEARCH_MOVIES_REQUEST, o state é atualizado com o novo objeto em que o valor de loading é true e errorMessage é null.

Outra observação é que em nosso useEffect, agora estamos enviando uma ação com o payload como o array de filmes que retornou do servidor. Além disso, na nossa função search, estamos na verdade enviando três ações diferentes.

  • Uma ação é a SEARCH_MOVIES_REQUEST que atualiza nosso state, tornando loading = true e errorMessage = null.
  • Se a solicitação for bem-sucedida, enviamos outra ação com o tipo SEARCH_MOVIES_SUCCESS que atualiza nosso state, atualizando os valores loading = false e movies = action.payload, em que o payload é o array de filmes obtido do OMDB.
  • Se houver um erro, enviaremos uma ação diferente com o tipo SEARCH_MOVIES_FAILURE que atualiza nosso state, com os valores loading = false  e errorMessage = action.error, em que action.error é a mensagem de erro obtida do servidor.

Para saber mais sobre o hook useReducer vale a pena olhar a documentação oficial.

Conclusão

Uau!!! Andamos um longo caminho! Mas tenho certeza que você está tão animado quanto eu com as possibilidades na utilização dos hooks. Para mim, pessoalmente falando, é muito mais fácil apresentar o React aos iniciantes, porque não preciso explicar como as classes funcionam, como o this funciona, ou ainda como funciona o bind em JS, o que é incrível.

Falamos apenas em alguns hooks neste tutorial e nem abordamos recursos como o de criar nossos próprios hooks personalizados. Se você tem outros casos de uso para hooks ou já implementou seu próprio hook personalizado, comente sobre isso e faça parte dessa empolgação toda.

Para acessar o repositório do projeto deste artigo no GitHub, é só clicar aqui.