Artigo original: https://www.freecodecamp.org/news/state-management-with-react-hooks/

Desde o anúncio do React Hooks, centenas, senão milhares, de artigos, bibliotecas e cursos em vídeo sobre eles foram lançados. Se você examinar cuidadosamente o mar de recursos, encontrará um artigo que escrevi há algum tempo que envolvia a construção de um aplicativo de amostra usando Hooks. Você pode encontrar esse artigo aqui.

Com base nesse artigo, muitas pessoas (na verdade, duas) fizeram perguntas relacionadas a como o state pode ser gerenciado em uma aplicação do React usando apenas Context e Hooks, o que me levou a fazer uma pequena pesquisa sobre o assunto.

Portanto, para este artigo, trabalharemos com um padrão para gerenciar o state usando dois Hooks muito importantes, useContext e useReducer, para construir um aplicativo simples de galeria de música. O aplicativo terá apenas duas telas: uma para login e outra para listar as músicas daquela galeria.

A principal razão para a página de login é mostrar como podemos compartilhar o state Auth em todo o aplicativo, que é um caso de uso comum para aplicativos que usam uma biblioteca como o Redux.

Quando terminarmos, devemos ter um aplicativo parecido com essas duas imagens:

login-page-1
Página de login
home-page-1
Página inicial (usei o nome “hooked” de novo - esperto, não?)

Para o servidor de back-end, configurei uma aplicação do Express simples e a hospedei no Heroku. Ela possui dois endpoints principais:

  • /login —Para autenticação. No login bem-sucedido, retorna um token JWT e os detalhes do usuário.
  • /songs — Retorna uma lista de músicas.

Caso você queira adicionar funcionalidades extras, o repositório para o aplicativo de back-end é esse aqui.

Recapitulando

Antes de começarmos a construir o aplicativo, vamos ver alguns dos hooks que usaremos:

  • useState — Este hook nos permite usar o state em componentes funcionais (o equivalente ao this.state e this.setState dos componentes de classe)
  • useContext — Este hook recebe um objeto de contexto e retorna o que for passado como uma propriedade de valor em MyContext.Provider. Se você não sabe nada sobre o contexto, ele é uma forma de passar o state de um componente pai para qualquer componente dentro da árvore (não importa qual seja a sua profundidade) sem ter que passá-lo por outros componentes que não necessitem dele (um problema apropriadamente chamado de perfuração de propriedades). Você pode ler mais sobre contexto aqui.
  • useReducer — Esta é uma alternativa ao useState e pode ser usada para lógicas complexas de state. Este é o meu hook favorito, porque funciona exatamente como a biblioteca do Redux. Ele aceita um reducer do tipo:
(state, action) => newState

E também recebe um objeto de state inicial antes de retornar o novo state.

VAMOS COMEÇAR

Usaremos o create-react-app para inicializar o projeto. Antes disso, porém, abaixo estão alguns dos requisitos necessários:

  • Node (≥ 6)
  • Uma IDE ou editor de texto

No seu terminal, digite o comando:

npx create-react-app hooked

Se não tiver o npx instalado, você também conseguirá instalar o create-react-app globalmente em seu sistema assim:

npm install -g create-react-app
create-react-app hooked

Você criará cinco componentes até o final deste artigo:

  • Header.js — Este componente conterá o cabeçalho do aplicativo, além de exibir um botão de logout que contém o primeiro nome do usuário. O botão só será exibido se o usuário estiver autenticado.
  • App.js — Este é o componente de alto nível onde criaremos o AuthContext (falarei sobre isso depois). Esse componente também renderizará condicionalmente o componente Login, se o usuário não estiver logado, ou o componente Home, se o usuário estiver autenticado.
  • Home.js — Este componente buscará uma lista de músicas do servidor e a renderizará na página.
  • Login.js — Este componente conterá o formulário de login do usuário. Ele também será responsável por fazer uma solicitação de POST para o endpoint de login e por atualizar o contexto de autenticação com a resposta do servidor.
  • Card.js — Este é um componente de interface do usuário (UI) que renderizará os detalhes da música que for passada para ele.

Agora, vamos criar os componentes vazios aos quais adicionaremos a lógica posteriormente. Na pasta src, crie uma pasta com o nome components, crie quatro arquivos, que serão os Header.js, Home.js, Login.js e Card.js:


Header.js

import React from "react";
export const Header = () => {
  return (
    <nav id="navigation">
      <h1 href="#" className="logo">
        HOOKED
      </h1>
    </nav>
  );
};
export default Header;

Home.js

import React from "react";
export const Home = () => {
return (
    <div className="home">
    </div>
  );
};
export default Home;

Login.js

import React from "react";
import logo from "../logo.svg";
import { AuthContext } from "../App";
export const Login = () => {
return (
    <div className="login-container">
      <div className="card">
        <div className="container">
        </div>
      </div>
    </div>
  );
};
export default Login;

App.js

import React from "react";
import "./App.css";
function App() {
return (
      <div className="App"></div>
  );
}
export default App;

No arquivo App.js, criaremos o AuthContext que passará o state de autenticação deste componente para qualquer outro componente que o exija. Crie o contexto de autorização igual a este abaixo:

import React from "react";
import "./App.css";
import Login from "./components/Login";
import Home from "./components/Home";
import Header from "./components/Header";
export const AuthContext = React.createContext(); // added this
function App() {
return (
    <AuthContext.Provider>
      <div className="App"></div>
    </AuthContext.Provider>
  );
}
export default App;

Em seguida, adicionamos o hook useReducer para lidar com nosso state de autenticação e para renderizar condicionalmente o componente Login ou o componente Home.

Lembre-se que o hook useReducer recebe dois parâmetros: um reducer (que é simplesmente uma função que recebe como parâmetros o state e a ação e que retorna um novo state baseado na ação) e um state inicial, que será passado para o reducer. Agora, adicionamos o hook em nosso componente App:

import React from "react";
import "./App.css";
import Login from "./components/Login";
import Home from "./components/Home";
import Header from "./components/Header";
export const AuthContext = React.createContext();
const initialState = {
  isAuthenticated: false,
  user: null,
  token: null,
};
const reducer = (state, action) => {
  switch (action.type) {
    case "LOGIN":
      localStorage.setItem("user", JSON.stringify(action.payload.user));
      localStorage.setItem("token", JSON.stringify(action.payload.token));
      return {
        ...state,
        isAuthenticated: true,
        user: action.payload.user,
        token: action.payload.token
      };
    case "LOGOUT":
      localStorage.clear();
      return {
        ...state,
        isAuthenticated: false,
        user: null
      };
    default:
      return state;
  }
};
function App() {
  const [state, dispatch] = React.useReducer(reducer, initialState);
return (
    <AuthContext.Provider
      value={{
        state,
        dispatch
      }}
    >
      <Header />
      <div className="App">{!state.isAuthenticated ? <Login /> : <Home />}</div>
    </AuthContext.Provider>
  );
}
export default App;

Tem bastante coisa acontecendo no código acima, eu sei. Fique tranquilo! Explicarei cada parte:

const initialState = {
  isAuthenticated: false,
  user: null,
  token: null,
};

O trecho acima é nosso objeto do state inicial que será usado em nosso reducer. Os valores neste objeto dependem principalmente de onde serão utilizados. No nosso caso, precisamos verificar se um usuário está autenticado, se contém os dados de user e se o token foi retornado do servidor após o login.

const reducer = (state, action) => {
  switch (action.type) {
    case "LOGIN":
      localStorage.setItem("user", JSON.stringify(action.payload.user));
      localStorage.setItem("token", JSON.stringify(action.payload.token));
      return {
        ...state,
        isAuthenticated: true,
        user: action.payload.user,
        token: action.payload.token
      };
    case "LOGOUT":
      localStorage.clear();
      return {
        ...state,
        isAuthenticated: false,
        user: null,
        token: null,
      };
    default:
      return state;
  }
};

A função reducer contém uma instrução case-switch que, com base em determinadas ações, retorna um novo state. As ações no reducer são:

  • LOGIN — Quando este tipo de ação e enviado, também será retornado um envio com payload (contendo o user e token). Ele agora salva o usuário e o token no localStorage e, então, retorna um novo state, definindo isAuthenticated como true, além do user e do token, com seus respectivos valores com base no payload da ação.
  • LOGOUT — Quando esta ação é enviada, limpamos o localStorage de todos os dados e definimos user e token como null.

Se nenhuma ação for enviada, ele retornará o state inicial.

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

O hook useReducer retorna dois parâmetros, o state e o dispatch. O state contém o estado que é usado no componente e é atualizado com base nas ações enviadas. Já o Dispatch é uma função que é usada no aplicativo para chamar/enviar essas ações que transformam ou alteram o state.

<AuthContext.Provider
      value={{
        state,
        dispatch
      }}
    >
      <Header />
      <div className="App">{!state.isAuthenticated ? <Login /> : <Home />}</div>
 </AuthContext.Provider>

Aqui no componente AuthContext.Provider, estamos passando um objeto para a propriedade value. O objeto contém o state e a função dispatch para que possa ser usado por qualquer outro componente que exija esse contexto. Em seguida, renderizamos condicionalmente os componentes – se o usuário for autenticado, renderizamos o componente Home. Caso contrário, renderizamos o componente Login.

Componente de login

No componente de login, vamos adicionar os elementos necessários para o formulário, assim:

import React from "react";
export const Login = () => {
return (
    <div className="login-container">
      <div className="card">
        <div className="container">
          <form>
            <h1>Login</h1>
			
    		<label htmlFor="email">
              Email Address
              <input
                type="text"
                name="email"
                id="email"
              />
            </label>
			
    		<label htmlFor="password">
              Password
              <input
                type="password"
                name="password"
                id="password"
              />
            </label>
			
    		<button>
                "Login"
            </button>
          
    	  </form>
        </div>
      </div>
    </div>
  );
};
export default Login;

No código acima, adicionamos o JSX que exibe o formulário. Em seguida, adicionaremos o hook useState para manipular o state do formulário. Depois de adicionado o hook, nosso código fica assim:

import React from "react";
export const Login = () => {
  const initialState = {
    email: "",
    password: "",
    isSubmitting: false,
    errorMessage: null
  };
const [data, setData] = React.useState(initialState);
const handleInputChange = event => {
    setData({
      ...data,
      [event.target.name]: event.target.value
    });
  };
return (
    <div className="login-container">
      <div className="card">
        <div className="container">
          <form>
            <h1>Login</h1>

    		<label htmlFor="email">
              Email Address
              <input
                type="text"
                value={data.email}
                onChange={handleInputChange}
                name="email"
                id="email"
              />
            </label>

			<label htmlFor="password">
              Password
              <input
                type="password"
                value={data.password}
                onChange={handleInputChange}
                name="password"
                id="password"
              />
            </label>

		{data.errorMessage && (
              <span className="form-error">{data.errorMessage}</span>
            )}

            <button disabled={data.isSubmitting}>
              {data.isSubmitting ? (
                "Loading..."
              ) : (
                "Login"
              )}
            </button>
          </form>
        </div>
      </div>
    </div>
  );
};
export default Login;

No código acima, passamos um objeto initialState para o hook useState. No objeto tratamos os states do e-mail, da senha, outro que é usado para verificar se o formulário está sendo enviado ao servidor e, também, um quarto valor, errorMessage, que trata os erros do servidor.

Depois, adicionaremos uma função que trata do envio do formulário para a API do back-end. Nessa função, usaremos o fetch para enviar o payload à API do servidor. Se a resposta for bem-sucedida, enviaremos uma ação de LOGIN e também passaremos a resposta do servidor como um payload na ação enviada. Se houver um erro do servidor (se as credenciais de login não forem válidas), chamamos o setData e passamos o errorMessage do servidor, que será exibido no formulário. Para chamar o dispatch, precisamos importar o AuthContext do componente App para o nosso componente Login e, em seguida, usar a função dispatch no aplicativo. No final, seu componente Login deve ficar assim:

import React from "react";
import { AuthContext } from "../App";
export const Login = () => {
  const { dispatch } = React.useContext(AuthContext);
  const initialState = {
    email: "",
    password: "",
    isSubmitting: false,
    errorMessage: null
  };
const [data, setData] = React.useState(initialState);
const handleInputChange = event => {
    setData({
      ...data,
      [event.target.name]: event.target.value
    });
  };
const handleFormSubmit = event => {
    event.preventDefault();
    setData({
      ...data,
      isSubmitting: true,
      errorMessage: null
    });
    fetch("https://hookedbe.herokuapp.com/api/login", {
      method: "post",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        username: data.email,
        password: data.password
      })
    })
      .then(res => {
        if (res.ok) {
          return res.json();
        }
        throw res;
      })
      .then(resJson => {
        dispatch({
            type: "LOGIN",
            payload: resJson
        })
      })
      .catch(error => {
        setData({
          ...data,
          isSubmitting: false,
          errorMessage: error.message || error.statusText
        });
      });
  };
return (
    <div className="login-container">
      <div className="card">
        <div className="container">
          <form onSubmit={handleFormSubmit}>
            <h1>Login</h1>

			<label htmlFor="email">
              Email Address
              <input
                type="text"
                value={data.email}
                onChange={handleInputChange}
                name="email"
                id="email"
              />
            </label>

			<label htmlFor="password">
              Password
              <input
                type="password"
                value={data.password}
                onChange={handleInputChange}
                name="password"
                id="password"
              />
            </label>

			{data.errorMessage && (
              <span className="form-error">{data.errorMessage}</span>
            )}

           <button disabled={data.isSubmitting}>
              {data.isSubmitting ? (
                "Loading..."
              ) : (
                "Login"
              )}
            </button>
          </form>
        </div>
      </div>
    </div>
  );
};
export default Login;

Componente Home

O componente Home tratará de buscar as músicas do servidor e exibi-las. Como o endpoint da API exige que enviemos o token de autenticação, precisaremos encontrar uma maneira de obtê-lo do componente App em que foi armazenado.

Vamos construir o html desse componente. Queremos buscar as músicas e mapear a lista de músicas retornadas e, então, renderizar um componente Card para cada música. Esse Card será um componente funcional simples, que será passado algumas props para que ele as renderize. Então, crie um arquivo Card.js na pasta components e, nesse arquivo, adicione o seguinte código:

import React from "react";
export const Card = ({ song }) => {
    
  return (
    <div className="card">
      <img
        src={song.albumArt}
        alt=""
      />
      <div className="content">
        <h2>{song.name}</h2>
        <span>BY: {song.artist}</span>
      </div>
    </div>
  );
};
export default Card;

Por conta dele não lidar com nenhuma lógica personalizada, apenas renderizar as props passadas para ele, nós o chamamos de componente de apresentação.

Agora, voltando ao nosso componente Home, ao lidar com requisições externas, na maioria dos aplicativos, tentamos visualizar três states principais. Primeiro, quando a requisição está sendo processada (usando algum tipo de indicador de carregamento). Depois, quando a requisição é bem-sucedida (renderizando o payload ou mostrando uma notificação de sucesso) ou quando ela falhar (mostrando uma notificação do erro ocorrido). Para fazer uma requisição quando o componente estiver montado e também para manipular esses três estados, usaremos os hooks useEffect e useReducer.

Para nosso hook useReducer, criaremos primeiro um objeto para manter o state inicial do nosso reducer. O objeto de state inicial ficará assim:

const initialState = {
  songs: [],
  isFetching: false,
  hasError: false,
};

Na propriedade songs, fica a lista de músicas recuperadas do servidor, inicialmente definida como vazia. A prop isFetching é usada para representar o estado do carregamento e está definida como false. Já a prop hasError é usada para representar o estado de erro e também é definida inicialmente como false.

Agora, podemos criar o reducer para o componente, que ficará parecido com isso:

const reducer = (state, action) => {
  switch (action.type) {
    case "FETCH_SONGS_REQUEST":
      return {
        ...state,
        isFetching: true,
        hasError: false
      };
    case "FETCH_SONGS_SUCCESS":
      return {
        ...state,
        isFetching: false,
        songs: action.payload
      };
    case "FETCH_SONGS_FAILURE":
      return {
        ...state,
        hasError: true,
        isFetching: false
      };
    default:
      return state;
  }
};

Bom, vamos fazer sua decomposição. Se enviarmos uma ação FETCH_SONGS_REQUEST em nosso aplicativo, retornaremos um novo state com o valor de isFetching definido como true. Se enviarmos uma ação FETCH_SONGS_SUCCESS, retornaremos um novo state com o valor de isFetching definido como false e definiremos songs com o payload retornado pelo servidor. Por fim, se enviarmos uma ação FETCH_SONGS_FAILURE, retornaremos um novo state com isFetching definido como false e hasError como true.

Agora que temos o hook useReducer, nosso componente Home deve ficar assim:

import React from "react";
import { AuthContext } from "../App";
import Card from "./Card";
const initialState = {
  songs: [],
  isFetching: false,
  hasError: false,
};
const reducer = (state, action) => {
  switch (action.type) {
    case "FETCH_SONGS_REQUEST":
      return {
        ...state,
        isFetching: true,
        hasError: false
      };
    case "FETCH_SONGS_SUCCESS":
      return {
        ...state,
        isFetching: false,
        songs: action.payload
      };
    case "FETCH_SONGS_FAILURE":
      return {
        ...state,
        hasError: true,
        isFetching: false
      };
    default:
      return state;
  }
};
export const Home = () => {
  const [state, dispatch] = React.useReducer(reducer, initialState);
return (
    <div className="home">
      {state.isFetching ? (
        <span className="loader">LOADING...</span>
      ) : state.hasError ? (
        <span className="error">OCORREU UM ERRO</span>
      ) : (
        <>
          {state.songs.length > 0 &&
            state.songs.map(song => (
              <Card key={song.id.toString()} song={song} />
            ))}
        </>
      )}
    </div>
  );
};
export default Home;

Para vermos rapidamente o que está acontecendo, dentro da função Home, adicionamos o hook useReducer e passamos o reducer e o initialState que, por sua vez, retorna duas variáveis, o state e o dispatch.

Então, em nossa função render, renderizamos condicionalmente a span com o texto "LOADING…" se state.isFetching = true, ou renderizamos a span com uma mensagem de erro se state.hasError = true. Caso contrário, percorremos a lista de músicas e renderizamos cada uma como um componente Card, passando as props.

Para amarrar tudo, adicionaremos a função useEffect, que tratará das requisições e enviará a ACTION necessária baseada na resposta do servidor. Adicionar o hook deixará nosso componente Home parecido com o trecho abaixo:

import React from "react";
import { AuthContext } from "../App";
import Card from "./Card";
const initialState = {
  songs: [],
  isFetching: false,
  hasError: false,
};
const reducer = (state, action) => {
  switch (action.type) {
    case "FETCH_SONGS_REQUEST":
      return {
        ...state,
        isFetching: true,
        hasError: false
      };
    case "FETCH_SONGS_SUCCESS":
      return {
        ...state,
        isFetching: false,
        songs: action.payload
      };
    case "FETCH_SONGS_FAILURE":
      return {
        ...state,
        hasError: true,
        isFetching: false
      };
    default:
      return state;
  }
};
export const Home = () => {
  const { state: authState } = React.useContext(AuthContext);
  const [state, dispatch] = React.useReducer(reducer, initialState);
React.useEffect(() => {
    dispatch({
      type: "FETCH_SONGS_REQUEST"
    });
    fetch("https://hookedbe.herokuapp.com/api/songs", {
      headers: {
        Authorization: `Bearer ${authState.token}`
      }
    })
      .then(res => {
        if (res.ok) {
          return res.json();
        } else {
          throw res;
        }
      })
      .then(resJson => {
        console.log(resJson);
        dispatch({
          type: "FETCH_SONGS_SUCCESS",
          payload: resJson
        });
      })
      .catch(error => {
        console.log(error);
        dispatch({
          type: "FETCH_SONGS_FAILURE"
        });
      });
  }, [authState.token]);

  return (
    <React.Fragment>
    <div className="home">
      {state.isFetching ? (
        <span className="loader">LOADING...</span>
      ) : state.hasError ? (
        <span className="error">OCORREU UM ERRO</span>
      ) : (
        <>
          {state.songs.length > 0 &&
            state.songs.map(song => (
              <Card key={song.id.toString()} song={song} />
            ))}
        </>
      )}
    </div>
    </React.Fragment>
  );
};
export default Home;

Se você percebeu, no código acima, usamos outro hook, o useContext. O motivo é que, para buscar músicas do servidor, também temos que passar o token de autorização retornado da requisição da página de login. Como esse era outro componente, contudo, armazenamos o token no AuthContext e usamos o hook useContext para obter esse valor de contexto e usá-lo nesse componente.

Dentro da função useEffect, enviamos inicialmente o FETCH_SONGS_REQUEST para que a span do carregamento seja exibida. Depois, fazemos a requisição usando o fetch  e passando o token que recebemos do AuthContext como cabeçalho. Se a resposta for bem-sucedida, enviamos a ação FETCH_SONGS_SUCCESS e passamos a lista de músicas retornadas do servidor como payload na ação. Se retornar um erro do servidor, enviamos a ação FETCH_SONGS_FAILURE para que o span de erro seja exibido na tela.

A última coisa a ser observada no nosso useEffect é que passamos o token no array de dependências do hook (leia mais sobre o useEffect aqui). Isso significa que ele só será chamado quando esse token mudar, e que só pode acontecer se o token expirar e precisarmos buscar um novo ou logarmos como um usuário diferente. Portanto, para este usuário, o hook será chamado apenas uma vez.

Ufa! Quanto código, hein? O lado bom é que já terminamos com toda a lógica do aplicativo. Agora, só falta o CSS. Porém, como entrar nos detalhes do estilo do aplicativo está além do escopo deste artigo, você pode copiar o trecho de CSS abaixo e colá-lo no arquivo App.css:

/******  LOGIN PAGE  ******/
.login-container{
  display: flex;
  align-items: center;
  background-image: url("./assets/carry-on-colour.svg");
  height: calc(100vh - 70px);
  background-repeat: no-repeat;
  background-position: right;
  padding-left: 5%;
  padding-right: 5%;
  margin-top: 70px;
}
.card {
  /* Add shadows to create the "card" effect */
  box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
  transition: 0.3s;
  height: 70%;
  width: 45%;
}
/* On mouse-over, add a deeper shadow */
.card:hover {
  box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
}
/* Add some padding inside the card container */
.login-container .container {
  padding-left: 7%;
  padding-right: 7%;
  height: 100%;
}
.login-container .container h1{
  font-size: 2.5rem;
}
.login-container .container form{
  display: flex;
  height: 80%;
  flex-direction: column;
  justify-content: space-around;
  align-self: center;
}
input[type="text"], input[type="password"]{
  padding-left: 1px;
  padding-right: 1px;
  height: 40px;
  border-radius: 5px;
  border: .5px solid rgb(143, 143, 143);
  font-size: 15px;
}
label{
  display: flex;
  flex-direction: column;
}
.login-container button{
  height: 40px;
  font-weight: bold;
  font-size: 15px;
  background-color: #F42B4B;
  color: rgb(255, 255, 255);
}
.login-container button:hover{
  background-color: rgb(151, 25, 46);
  cursor: pointer;
}
.login-container button:focus{
  outline: none !important;
}


.spinner {
  animation: spinner infinite .9s linear;
  height: 90%;
}
.spinner:focus{
  border:none;
}
@keyframes spinner {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}
.form-error{
  color: #F42B4B;
  text-align: center;
}
@media screen and (max-width: 700px){
  .login-container{
    justify-content: center;
    background-image: none;
  }
  .card {
    width: 80%;
    align-self: center;
  }
  
}
@media screen and (max-width: 350px){
  .card {
    width: 100%;
  }
  
}
/******  LOGIN PAGE  ******/


/******  HEADER  ******/
#navigation{
  width: 100%;
  position: fixed;
  z-index: 10;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  background-color: #F42B4B;
  box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
  height: 70px;
  top: 0;
  padding-right: 5px;
  padding-left: 5px;
}
#navigation h1{
  color: white;
}
#navigation button{
  background-color: transparent;
  border: none;
  align-self: center;
}
#navigation button:hover{
  cursor: pointer;
}
#navigation button:focus{
  outline: none !important;
}
/******  HEADER  ******/


/******  HOME PAGE  ******/
.home {
  margin-top: 100px;
  margin-left: 2%;
  margin-right: 2%;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
}
.home .loader{
  align-self: center;
  width: 100%;
  text-align: center;
}
.home .error{
  width: 100%;
  align-self: center;
  color: #F42B4B;
  font-size: 30px;
  font-weight: bold;
  text-align: center;
}
.home>.card {
  /* Add shadows to create the "card" effect */
  box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
  transition: 0.3s;
  height: 400px;
  width: 30%;
  position: relative;
  margin-bottom: 2%;
}
/* On mouse-over, add a deeper shadow */
.home .card:hover {
  box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
}
.home .card>img{
  width: 100%;
  height: 100%;
}
.home .content{
  bottom: 0;
  z-index: 9;
  position: absolute;
  background-color: rgba(255, 255, 255, 0.7);
  display: flex;
  flex-direction: column;
  width: 100%;
  align-items: center;
  height: 35%;
  padding-bottom: 5px;
  transition: 0.5s;
}
.home .content:hover{
  background-color: rgba(255, 255, 255, 1);
  height: 50%;
  cursor: pointer;
}
.content>h2{
  text-align: center;
  font-size: 2rem;
}
@media screen and (max-width: 780px){
.home{
    justify-content: space-around;
  }
  .home .card {
    width: 45%;
  }
}
@media screen and (max-width: 500px){
  .home .card {
    width: 90%;
  }
}
@media screen and (min-width: 1400px){
  .home {
    margin: auto;
    width: 1400px;
  }
  .toggle-button{
    margin-bottom: 10px;
  }
}
/******  HOME PAGE  ******/

Este artigo foi um pouco longo, mas espero que ele cubra um caso de uso comum com o uso de hooks para gerenciar o state de nosso aplicativo.

Você pode acessar o repositório desse projeto no GitHub clicando aqui. Observe que o repositório possui alguns recursos adicionais, como a parte de criação de uma nova música.