Criando um simples Pokemon Web App com React Hooks e API de contexto

const [pokemons] = useState([
  { id: 1, name: 'Bulbasaur' },
  { id: 2, name: 'Charmander' },
  { id: 3, name: 'Squirtle' }
]);

Aqui temos uma lista de três objetos Pokemon. o useState O hook fornece um par de itens: o estado atual e uma função para permitir que você atualize esse estado criado.

Agora, com o estado de pokemons, podemos mapeá-lo e renderizar o nome de cada um.

{pokemons.map((pokemon) => 

{pokemon.name}

)}

É apenas um mapa retornando o nome de cada Pokémon em uma tag de parágrafo.

Este é todo o componente implementado:

import React, { useState } from 'react';

const PokemonsList = () => {
  const [pokemons] = useState([
    { id: 1, name: 'Bulbasaur' },
    { id: 2, name: 'Charmander' },
    { id: 3, name: 'Squirtle' }
  ]);

  return (
    

Pokemons List

{pokemons.map((pokemon) =>

{pokemon.id}

{pokemon.name}

)}
) } export default PokemonsList;

Apenas um pequeno ajuste aqui:

  • Adicionado o key em uma combinação de pokemon id e name
  • E renderize um parágrafo para o id atributo (eu estava apenas testando. Mas iremos removê-lo mais tarde)

Ótimo! Agora temos a primeira lista em funcionamento.

Eu quero fazer essa mesma implementação, mas agora para os pokemons capturados. Mas para os pokemons capturados, quero fazê-los como uma lista vazia. Porque quando o “jogo” começa, eu não tenho nenhum Pokémon capturado, certo? Direita!

const [pokemons] = useState([]);

É isso mesmo, muito simples!

Todo o componente é semelhante ao outro:

import React, { useState } from 'react';

const CapturedPokemons = () => {
  const [pokemons] = useState([]);

  return (
    

Captured Pokemons

{pokemons.map((pokemon) =>

{pokemon.id}

{pokemon.name}

)}
) } export default CapturedPokemons;

Aqui mapeamos, mas como o array está vazio, ele não renderiza nada.

Agora que tenho os dois componentes principais, posso reuni-los no App componente:

import React from 'react';
import './App.css';

import PokemonsList from './PokemonsList';
import Pokedex from './Pokedex';

const App = () => (
  
); export default App;

Capturando e liberando

Esta é a segunda parte do nosso aplicativo. Vamos capturar e liberar Pokemons. Então, vamos pensar no comportamento esperado.

Para cada Pokémon da lista de Pokemons disponíveis, desejo ativar uma ação para capturá-los. A ação de captura os removerá da lista em que estavam e os adicionará à lista de Pokemons capturados.

A ação de liberação terá um comportamento semelhante. Mas, em vez de passar da lista disponível para a lista capturada, será o contrário. Vamos movê-los da lista capturada para a lista disponível.

Portanto, as duas caixas precisam compartilhar dados para poder adicionar o Pokemon à outra lista. Como fazemos isso, pois são componentes diferentes no aplicativo? Vamos falar sobre a API do React Context.

A API de contexto foi projetada para criar dados globais para uma árvore definida de componentes do React. Como os dados são globais, podemos compartilhá-los entre os componentes nesta árvore definida. Então, vamos usá-lo para compartilhar nossos dados simples de Pokemon entre as duas caixas.

Nota mental: “O contexto é usado principalmente quando alguns dados precisam ser acessíveis por muitos componentes em diferentes níveis de aninhamento”. – Reagir documentos.

Usando a API, simplesmente criamos um novo contexto como esse:

import { createContext } from 'react';

const PokemonContext = createContext();

Agora, com o PokemonContext, podemos usar seu provedor. Ele funcionará como um invólucro de componente de uma árvore de componentes. Ele fornece dados globais para esses componentes e permite que eles se inscrevam para quaisquer alterações relacionadas a esse contexto. Se parece com isso:


o value prop é apenas um valor que esse contexto fornece aos componentes agrupados. O que devemos fornecer às listas disponíveis e capturadas?

  • pokemons: para listar na lista disponível
  • capturedPokemons: para listar na lista capturada
  • setPokemons: para poder atualizar a lista disponível
  • setCapturedPokemons: para poder atualizar a lista capturada

Como mencionei antes no useState Em parte, esse gancho sempre fornece um par: o estado e uma função para atualizar esse estado. Esta função manipula e atualiza o estado do contexto. Em outras palavras, eles são os setPokemons e setCapturedPokemons. Como?

const [pokemons, setPokemons] = useState([
  { id: 1, name: 'Bulbasaur' },
  { id: 2, name: 'Charmander' },
  { id: 3, name: 'Squirtle' }
]);

Agora nós temos o setPokemons.

const [capturedPokemons, setCapturedPokemons] = useState([]);

E agora também temos o setCapturedPokemons.

Com todos esses valores em mãos, agora podemos passá-los para o fornecedor value suporte.

import React, { createContext, useState } from 'react';

export const PokemonContext = createContext();

export const PokemonProvider = (props) => {
  const [pokemons, setPokemons] = useState([
    { id: 1, name: 'Bulbasaur' },
    { id: 2, name: 'Charmander' },
    { id: 3, name: 'Squirtle' }
  ]);

  const [capturedPokemons, setCapturedPokemons] = useState([]);

  const providerValue = {
    pokemons,
    setPokemons,
    capturedPokemons,
    setCapturedPokemons
  };

  return (
    
      {props.children}
    
  )
};

Eu criei um PokemonProvider agrupar todos esses dados e APIs para criar o contexto e retornar o provedor de contexto com o valor definido.

Mas como fornecemos esses dados e APIs para o componente? Precisamos fazer duas coisas principais:

  • Enrole os componentes neste provedor de contexto
  • Use o contexto em cada componente

Vamos envolvê-los primeiro:

const App = () => (
  
    
);

E usamos o contexto usando o useContext e passando o criado PokemonContext. Como isso:

import { useContext } from 'react';
import { PokemonContext } from './PokemonContext';

useContext(PokemonContext); // returns the context provider value we created

Para os pokemons disponíveis, queremos capturá-los, por isso seria útil ter o setCapturedPokemons API de função para atualizar os pokemons capturados. Como o pokemon é capturado, precisamos removê-lo da lista disponível. setPokemons também é necessário aqui. E para atualizar cada lista, precisamos dos dados atuais. Então, basicamente, precisamos de tudo, desde o provedor de contexto.

Precisamos criar um botão com uma ação para capturar o pokemon:


  • o capture A função atualizará o pokemons e a capturedPokemons listas

const capture = (pokemon) => (event) => {
  // update captured pokemons list
  // update available pokemons list
};

Para atualizar o capturedPokemons, podemos chamar o setCapturedPokemons função com a corrente capturedPokemons e o pokemon a ser capturado.

setCapturedPokemons([...capturedPokemons, pokemon]);

E para atualizar o pokemons lista, basta filtrar o pokemon que será capturado.

setPokemons(removePokemonFromList(pokemon));

removePokemonFromList é apenas uma função simples para filtrar os pokemons removendo o pokemon capturado.

const removePokemonFromList = (removedPokemon) =>
  pokemons.filter((pokemon) => pokemon !== removedPokemon)

Como o componente se parece agora?

import React, { useContext } from 'react';
import { PokemonContext } from './PokemonContext';

export const PokemonsList = () => {
  const {
    pokemons,
    setPokemons,
    capturedPokemons,
    setCapturedPokemons
  } = useContext(PokemonContext);

  const removePokemonFromList = (removedPokemon) =>
    pokemons.filter(pokemon => pokemon !== removedPokemon);

  const capture = (pokemon) => () => {
    setCapturedPokemons([...capturedPokemons, pokemon]);
    setPokemons(removePokemonFromList(pokemon));
  };

  return (
    

Pokemons List

{pokemons.map((pokemon) =>
{pokemon.name}
)}
); }; export default PokemonsList;

Será muito parecido com o componente de pokemons capturados. Ao invés de capture, será uma release função:

import React, { useContext } from 'react';
import { PokemonContext } from './PokemonContext';

const CapturedPokemons = () => {
  const {
    pokemons,
    setPokemons,
    capturedPokemons,
    setCapturedPokemons,
  } = useContext(PokemonContext);

  const releasePokemon = (releasedPokemon) =>
    capturedPokemons.filter((pokemon) => pokemon !== releasedPokemon);

  const release = (pokemon) => () => {
    setCapturedPokemons(releasePokemon(pokemon));
    setPokemons([...pokemons, pokemon]);
  };

  return (
    

CapturedPokemons

{capturedPokemons.map((pokemon) =>
{pokemon.name}
)}
); }; export default CapturedPokemons;

Reduzindo a complexidade

Agora usamos o useState, a API de contexto, provedor de contexto, o useContext. E mais importante, podemos compartilhar dados entre caixas de Pokemon.

Outra maneira de gerenciar o estado é usando useReducer como alternativa para useState.

O ciclo de vida do redutor funciona assim: o useReducer Fornece uma dispatch função. Com esta função, podemos despachar um action dentro de um componente. o reducer recebe a ação e o estado. Ele entende o tipo de ação, manipula os dados e retorna um novo estado. Agora, o novo estado pode ser usado no componente.

Como exercício, e para entender melhor esse gancho, tentei substituir o useState com isso.

o useState estava dentro do PokemonProvider. Podemos redefinir o estado inicial dos pokemons disponíveis e capturados nesta estrutura de dados:

const defaultState = {
  pokemons: [
    { id: 1, name: 'Bulbasaur' },
    { id: 2, name: 'Charmander' },
    { id: 3, name: 'Squirtle' }
  ],
  capturedPokemons: []
};

E passe esse valor para o useReducer:

const [state, dispatch] = useReducer(pokemonReducer, defaultState);

o useReducer recebe dois parâmetros: o redutor e o estado inicial. Vamos construir o pokemonReducer agora.

O redutor recebe o estado atual e a ação que foi despachada.

const pokemonReducer = (state, action) => // returns the new state based on the action type

Aqui obtemos o tipo de ação e retornamos um novo estado. A ação é um objeto. Se parece com isso:

{ type: 'AN_ACTION_TYPE' }

Mas também pode ser maior:

{
  type: 'AN_ACTION_TYPE',
  pokemon: {
    name: 'Pikachu'
  }
}

Este é o caso, passamos um Pokémon para o objeto de ação. Vamos fazer uma pausa por um minuto e pensar no que queremos fazer dentro do redutor.

Aqui, geralmente atualizamos dados e lidamos com ações. As ações são despachadas. Então, ações são comportamento. E o comportamento do nosso aplicativo é: capturar e liberar! Estas são as ações que precisamos lidar aqui.

É assim que nosso redutor será:

const pokemonReducer = (state, action) => {
  switch (action.type) {
    case 'CAPTURE':
      // handle capture and return new state
    case 'RELEASE':
      // handle release and return new state
    default:
      return state;
  }
};

Se nossa ação tiver um tipo CAPTURE, lidamos com isso de uma maneira. Se nosso tipo de ação é o RELEASE, lidamos com isso de outra maneira. Se o tipo de ação não corresponder a nenhum desses tipos, basta retornar o estado atual.

Quando capturamos o pokemon, precisamos atualizar as duas listas: remova o pokemon da lista disponível e adicione-o à lista capturada. É esse estado que precisamos retornar do redutor.

const getPokemonsList = (pokemons, capturedPokemon) =>
  pokemons.filter(pokemon => pokemon !== capturedPokemon)

const capturePokemon = (pokemon, state) => ({
  pokemons: getPokemonsList(state.pokemons, pokemon),
  capturedPokemons: [...state.capturedPokemons, pokemon]
});

o capturePokemon A função apenas retorna as listas atualizadas. o getPokemonsList remove o pokemon capturado da lista disponível.

E usamos esta nova função no redutor:

const pokemonReducer = (state, action) => {
  switch (action.type) {
    case 'CAPTURE':
      return capturePokemon(action.pokemon, state);
    case 'RELEASE':
      // handle release and return new state
    default:
      return state;
  }
};

Agora o release função!

const getCapturedPokemons = (capturedPokemons, releasedPokemon) =>
  capturedPokemons.filter(pokemon => pokemon !== releasedPokemon)

const releasePokemon = (releasedPokemon, state) => ({
  pokemons: [...state.pokemons, releasedPokemon],
  capturedPokemons: getCapturedPokemons(state.capturedPokemons, releasedPokemon)
});

o getCapturedPokemons remova o pokemon lançado da lista capturada. o releasePokemon A função retorna as listas atualizadas.

Nosso redutor se parece com isso agora:

const pokemonReducer = (state, action) => {
  switch (action.type) {
    case 'CAPTURE':
      return capturePokemon(action.pokemon, state);
    case 'RELEASE':
      return releasePokemon(action.pokemon, state);
    default:
      return state;
  }
};

Apenas um refator menor: tipos de ação! Essas são strings e podemos extraí-las em uma constante e fornecer para o despachante.

export const CAPTURE = 'CAPTURE';
export const RELEASE = 'RELEASE';

E o redutor:

const pokemonReducer = (state, action) => {
  switch (action.type) {
    case CAPTURE:
      return capturePokemon(action.pokemon, state);
    case RELEASE:
      return releasePokemon(action.pokemon, state);
    default:
      return state;
  }
};

O arquivo redutor inteiro fica assim:

export const CAPTURE = 'CAPTURE';
export const RELEASE = 'RELEASE';

const getCapturedPokemons = (capturedPokemons, releasedPokemon) =>
  capturedPokemons.filter(pokemon => pokemon !== releasedPokemon)

const releasePokemon = (releasedPokemon, state) => ({
  pokemons: [...state.pokemons, releasedPokemon],
  capturedPokemons: getCapturedPokemons(state.capturedPokemons, releasedPokemon)
});

const getPokemonsList = (pokemons, capturedPokemon) =>
  pokemons.filter(pokemon => pokemon !== capturedPokemon)

const capturePokemon = (pokemon, state) => ({
  pokemons: getPokemonsList(state.pokemons, pokemon),
  capturedPokemons: [...state.capturedPokemons, pokemon]
});

export const pokemonReducer = (state, action) => {
  switch (action.type) {
    case CAPTURE:
      return capturePokemon(action.pokemon, state);
    case RELEASE:
      return releasePokemon(action.pokemon, state);
    default:
      return state;
  }
};

Agora que o redutor está implementado, podemos importá-lo para o nosso provedor e usá-lo no useReducer gancho.

const [state, dispatch] = useReducer(pokemonReducer, defaultState);

Como estamos dentro do PokemonProvider, queremos agregar algum valor aos componentes que consomem: as ações de captura e liberação.

Essas funções só precisam despachar o tipo de ação correto e passar o pokemon para o redutor.

  • o capture function: recebe o pokemon e retorna uma nova função que despacha uma ação com o tipo CAPTURE e o pokemon capturado.

const capture = (pokemon) => () => {
  dispatch({ type: CAPTURE, pokemon });
};

  • o release function: recebe o pokemon e retorna uma nova função que despacha uma ação com o tipo RELEASE e o pokemon de lançamento.

const release = (pokemon) => () => {
  dispatch({ type: RELEASE, pokemon });
};

Agora, com o estado e as ações implementadas, podemos fornecer esses valores aos componentes consumidores. Basta atualizar o suporte ao valor do provedor.

const { pokemons, capturedPokemons } = state;

const providerValue = {
  pokemons,
  capturedPokemons,
  release,
  capture
};


  {props.children}

Ótimo! Agora, de volta ao componente. Vamos usar essas novas ações. Todas as lógicas de captura e liberação estão encapsuladas em nosso fornecedor e redutor. Nosso componente está bem limpo agora. o useContext ficará assim:

const { pokemons, capture } = useContext(PokemonContext);

E todo o componente:

import React, { useContext } from 'react';
import { PokemonContext } from './PokemonContext';

const PokemonsList = () => {
  const { pokemons, capture } = useContext(PokemonContext);

  return (
    

Pokemons List

{pokemons.map((pokemon) =>
{pokemon.name}
)}
) }; export default PokemonsList;

Para o componente de pokemons capturados, será muito parecido. o useContext:

const { capturedPokemons, release } = useContext(PokemonContext);

E todo o componente:

import React, { useContext } from 'react';
import { PokemonContext } from './PokemonContext';

const Pokedex = () => {
  const { capturedPokemons, release } = useContext(PokemonContext);

  return (
    

Pokedex

{capturedPokemons.map((pokemon) =>
{pokemon.name}
)}
) }; export default Pokedex;

Sem lógica. Apenas interface do usuário. Muito limpo.

Pokemon God: O Criador

Agora que temos a comunicação entre as duas listas, quero construir uma terceira caixa. É assim que criamos novos Pokemons. Mas é apenas uma simples entrada e botão de envio. Quando adicionamos um nome de pokemon na entrada e pressionamos o botão, ele enviará uma ação para adicionar esse pokemon à lista disponível.

Como precisamos acessar a lista disponível para atualizá-la, precisamos compartilhar o estado. Portanto, nosso componente será envolvido por nossos PokemonProvider junto com os outros componentes.

const App = () => (
  
    
);

Vamos construir o PokemonForm componente agora. O formulário é bem direto:

Temos um formulário, uma entrada e um botão. Para resumir, também temos uma função para manipular o envio do formulário e outra função para manipular a entrada na alteração.

o handleNameOnChange será chamado toda vez que o usuário digitar ou remover um caractere. Eu queria construir um estado local, uma representação do nome do pokemon. Com esse estado, podemos usá-lo para enviar ao enviar o formulário.

Como queremos experimentar ganchos, usaremos useState para lidar com esse estado local.

const [pokemonName, setPokemonName] = useState();

const handleNameOnChange = (e) => setPokemonName(e.target.value);

Nós usamos o setPokemonName para atualizar o pokemonName toda vez que o usuário interage com a entrada.

E a handleFormSubmit é uma função para despachar o novo pokemon a ser adicionado à lista disponível.

const handleFormSubmit = (e) => {
  e.preventDefault();
  addPokemon({
    id: generateID(),
    name: pokemonName
  });
};

o addPokemon é a API que criaremos mais tarde. Ele recebe o pokemon: id e nome. O nome é o estado local que definimos: pokemonName.

o generateID é apenas uma função simples que criei para gerar um número aleatório. Se parece com isso:

export const generateID = () => {
  const a = Math
    .random()
    .toString(36)
    .substring(2, 15)

  const b = Math
    .random()
    .toString(36)
    .substring(2, 15)

  return a + b;
};

o addPokemon será fornecido pela API de contexto que construímos. Dessa forma, esta função pode receber o novo Pokemon e adicioná-lo à lista disponível. Se parece com isso:

const addPokemon = (pokemon) => {
  dispatch({ type: ADD_POKEMON, pokemon });
};

Ele enviará esse tipo de ação ADD_POKEMON e também passar o Pokemon.

Em nosso redutor, adicionamos o argumento para o ADD_POKEMON e manipule o estado para adicionar o novo pokemon ao estado.

const pokemonReducer = (state, action) => {
  switch (action.type) {
    case CAPTURE:
      return capturePokemon(action.pokemon, state);
    case RELEASE:
      return releasePokemon(action.pokemon, state);
    case ADD_POKEMON:
      return addPokemon(action.pokemon, state);
    default:
      return state;
  }
};

E a addPokemon A função será:

const addPokemon = (pokemon, state) => ({
  pokemons: [...state.pokemons, pokemon],
  capturedPokemons: state.capturedPokemons
});

Outra abordagem é desestruturar o estado e alterar apenas o atributo pokemons. Como isso:

const addPokemon = (pokemon, state) => ({
  ...state,
  pokemons: [...state.pokemons, pokemon],
});

Voltando ao nosso componente, só precisamos fazer o useContext fornece o addPokemon API de expedição com base no PokemonContext:

const { addPokemon } = useContext(PokemonContext);

E todo o componente fica assim:

import React, { useContext, useState } from 'react';
import { PokemonContext } from './PokemonContext';
import { generateID } from './utils';

const PokemonForm = () => {
  const [pokemonName, setPokemonName] = useState();
  const { addPokemon } = useContext(PokemonContext);

  const handleNameOnChange = (e) => setPokemonName(e.target.value);

  const handleFormSubmit = (e) => {
    e.preventDefault();
    addPokemon({
      id: generateID(),
      name: pokemonName
    });
  };

  return (
    
); }; export default PokemonForm;

Agora temos a lista de pokemons disponíveis, a lista de pokemons capturados e a terceira caixa para criar novos pokemons.

Efeitos Pokemon

Agora que o aplicativo está quase completo, podemos substituir os pokemons zombados por uma lista de pokemons da PokeAPI.

Portanto, dentro do componente de função, não podemos causar efeitos colaterais como log ou assinaturas. É por isso que o useEffect gancho existe. Com esse gancho, podemos buscar pokemons (um efeito colateral) e adicioná-los à lista.

A busca do PokeAPI terá a seguinte aparência:

const url = "https://pokeapi.co/api/v2/pokemon";
const response = await fetch(url);
const data = await response.json();
data.results; // [{ name: 'bulbasaur', url: 'https://pokeapi.co/api/v2/pokemon/1/' }, ...]

o results atributo é a lista de pokemons buscados. Com esses dados, poderemos adicionar à lista de pokemons.

Vamos pegar o código de solicitação dentro do useEffect:

useEffect(() => {
  const fetchPokemons = async () => {
    const response = await fetch(url);
    const data = await response.json();
    data.results; // update the pokemons list with this data
  };

  fetchPokemons();
}, []);

Para poder async-await, precisamos criar uma função e chamá-la mais tarde. A matriz vazia é um parâmetro para tornar o useEffect conhece as dependências que procurará para executar novamente.

O comportamento padrão é executar o efeito de cada renderização concluída. Se adicionarmos uma dependência a esta lista, o useEffect só será executado novamente quando a dependência mudar, em vez de executar em todas as renderizações concluídas.

Agora, que buscamos os pokemons, precisamos atualizar a lista. É uma ação, um novo comportamento. Precisamos usar o despacho novamente, implementar um novo tipo no redutor e atualizar o estado no provedor de contexto.

No PokemonContext, criamos o addPokemons para fornecer uma API ao componente consumidor que a usa.

const addPokemons = (pokemons) => {
  dispatch({ type: ADD_POKEMONS, pokemons });
};

Ele recebe pokemons e despacha uma nova ação: ADD_POKEMONS.

No redutor, adicionamos esse novo tipo, esperamos os pokemons e chamamos uma função para adicionar os pokemons ao estado da lista disponível.

const pokemonReducer = (state, action) => {
  switch (action.type) {
    case CAPTURE:
      return capturePokemon(action.pokemon, state);
    case RELEASE:
      return releasePokemon(action.pokemon, state);
    case ADD_POKEMON:
      return addPokemon(action.pokemon, state);
    case ADD_POKEMONS:
      return addPokemons(action.pokemons, state);
    default:
      return state;
  }
};

o addPokemons função basta adicionar os pokemons à lista:

const addPokemons = (pokemons, state) => ({
  pokemons: pokemons,
  capturedPokemons: state.capturedPokemons
});

Podemos refatorar isso destruindo o estado e o valor da propriedade do objeto:

const addPokemons = (pokemons, state) => ({
  ...state,
  pokemons,
});

Como fornecemos essa API de função para o componente consumidor agora, podemos usar o useContext para obtê-la.

const { addPokemons } = useContext(PokemonContext);

Todo o componente se parece com isso:

import React, { useContext, useEffect } from 'react';
import { PokemonContext } from './PokemonContext';

const url = "https://pokeapi.co/api/v2/pokemon";

export const PokemonsList = () => {
  const { state, capture, addPokemons } = useContext(PokemonContext);

  useEffect(() => {
    const fetchPokemons = async () => {
      const response = await fetch(url);
      const data = await response.json();
      addPokemons(data.results);
    };    

    fetchPokemons();
  }, [addPokemons]);

  return (
    

Pokemons List

{state.pokemons.map((pokemon) =>
{pokemon.name}
)}
); }; export default PokemonsList;

Empacotando

Esta foi a minha tentativa de compartilhar meus aprendizados e experiências ao experimentar ganchos em um mini-projeto paralelo. Aprendemos a lidar com o estado local com useState, construindo um estado global com o Context API, como reescrever e substituir o useState com useReducere fazendo efeitos colaterais no useEffect.

Disclaimer: foi apenas um projeto experimental. Apenas para fins de aprendizado. Não tenho a resposta sobre as boas práticas de Hooks ou sobre como torná-lo escalável em grandes projetos.

Espero que tenha sido uma boa leitura! Continue aprendendo e codificando!

Recursos