Qual o tipo de um React Hook?
Você está pronto para embarcar em uma jornada na construção de componentes React com TypeScript? À medida que você se aprofunda no desenvolvimento front-end com a biblioteca React, logo perceberá a necessidade de especificar os tipos dos dados que são utilizados em seu código. É como escolher os Pokémon certos para a batalha, garantindo que eles sejam compatíveis com suas estratégias.
Neste artigo, abordaremos a tipagem dos hooks nativos do React. Além disso, vamos criar e aplicar tipos em um hook customizado. Você também aprenderá a lidar com tipos dinâmicos e inferência, assim como um treinador experiente adapta sua estratégia durante uma batalha Pokémon.
Hooks? Quem é esse Pokémon?
Os hooks do React são funções especiais que permitem que você "conecte" estados e comportamentos em componentes funcionais do React. Eles foram introduzidos no React 16.8 e proporcionaram uma maneira mais simples e eficiente de gerenciar estados e efeitos colaterais em componentes funcionais, tornando-os mais poderosos e flexíveis. Alguns dos hooks mais comuns incluem useState, useEffect, useContext, entre outros.
Eles ajudam os desenvolvedores a organizar o código de forma mais modular e reutilizável, facilitando o desenvolvimento de aplicativos React mais robustos e escaláveis. Você pode usar os hooks nativos do React ou criar os seus próprios hooks personalizados (custom hooks).
Hooks nativos do React
Os hooks nativos do React são aqueles que fazem parte da biblioteca React e são usados para criar e manipular o estado, os efeitos e o contexto dos componentes. Os principais hooks nativos são:
useState
O hook useState cria um estado local para um componente e retorna um array com duas posições: a primeira é o valor atual do estado e a segunda é uma função para atualizar esse valor.
Para entender melhor, vamos usar uma analogia com Pokemon. Imagine que você tem uma pokebola que pode guardar um Pokémon dentro dela. Você pode abrir a pokebola para ver qual Pokémon está dentro, ou trocar o Pokémon por outro. Essa pokebola é como o hook useState
: ela guarda um valor (o nome do Pokémon) e te dá uma forma de mudar esse valor (trocar o Pokémon).
Agora, imagine que você quer criar um componente React que mostra o nome do Pokémon na tela e um botão para trocá-lo por outro Pokémon aleatório. Você poderia usar um hook chamado useState
para criar uma pokebola que guarda o nome do Pokémon e uma função para trocá-lo. Veja como ficaria o código:
import { useState } from "react";
const Pokemons = ["Pikachu", "Charmander", "Bulbasaur", "Squirtle", "Eevee"];
Aqui definimos um array chamado Pokemons que contém uma lista de nomes de Pokémon. Cada elemento do array é uma string.
function getPokemonAleatorio() {
const index = Math.floor(Math.random() * Pokemons.length);
return Pokemons[index];
}
A função getPokemonAleatorio retorna um Pokémon aleatório da lista Pokemons que criamos anteriormente.
function Pokemon() {
const [Pokemon, setPokemon] = useState<string>('Pikachu');
function trocarPokemon() {
const novoPokemon = getPokemonAleatorio();
setPokemon(novoPokemon);
}
Aqui estamos criando um componente chamado Pokemon que utiliza o useState do React para gerenciar um estado chamado Pokemon. O estado é inicializado com o valor "Pikachu" como uma string. O componente também possui uma função chamada trocarPokemon. Ela chama a função getPokemonAleatorio() para obter um nome de Pokémon aleatório. Em seguida, atualiza o estado Pokemon com o novo nome de Pokémon obtido, utilizando a função setPokemon(novoPokemon).
return (
<div className="Pokemon">
<h1>{Pokemon}</h1>
<button onClick={trocarPokemon}>Trocar</button>
</div>
);
}
export default Pokemon;
Agora construímos a parte visual do componente incluindo um container, um título com o nome do Pokémon e um botão que chama a função trocarPokemon ao ser clicado, resultando em:
useEffect
O hook useEffect executa uma função sempre que alguma dependência mudar e retorna uma função opcional para limpar os efeitos colaterais.
Agora, imagine que você quer ver mais informações sobre o Pokémon que está dentro da sua pokebola, como a sua imagem e os seus tipos. Você poderia usar outro hook chamado useEffect para buscar esses dados de uma API externa quando o nome do Pokémon mudar. A API que utilizaremos é a PokeApi, uma API (Application Programming Interface) pública e gratuita que fornece informações detalhadas sobre Pokémon, incluindo dados sobre os próprios Pokémon, tipos, movimentos, evoluções, estatísticas, sprites (imagens) e muito mais.
Veja como ficaria o código:
import { useEffect, useState } from "react";
interface Pokemon {
nome: string;
imagem: string;
}
Estamos importando duas funções fornecidas pela biblioteca React: a useEffect e a useState. Além disso definimos uma interface TypeScript chamada "Pokemon", onde definimos que um objeto do tipo "Pokemon" deve ter duas propriedades obrigatórias: "nome" e "imagem". Ambas as propriedades são do tipo string, o que significa que esperamos que o nome do Pokémon e o caminho para a imagem sejam representados como strings.
function Pokemon({ nome }: { nome: string }) {
const [Pokemon, setPokemon] = useState<Pokemon | null>(null);
Aqui, estamos criando uma função componente chamada "Pokemon". Este componente recebe um único argumento chamado "nome", que é um objeto desestruturado com uma propriedade "nome" do tipo string. Isso significa que quando usarmos esse componente, precisaremos passar um valor string para a propriedade "nome". Dentro do componente, estamos usando o hook useState para criar um estado local.
Estamos inicializando o estado do Pokémon com null, o que significa que inicialmente não temos informações sobre o Pokémon. Além disso, estamos usando a notação <Pokemon | null>
para tipar o estado. Isso significa que o estado pode ser do tipo "Pokemon" (quando tivermos informações do Pokémon) ou null (quando ainda não tivermos informações).
useEffect(() => {
async function buscaPokemon() {
const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${nome}`);
const data = await response.json();
const Pokemon: Pokemon = {
nome: data.name,
imagem: data.sprites.front_default,
};
setPokemon(Pokemon);
}
if (nome) {
buscaPokemon();
}
}, [nome]);
O useEffect é um hook do React que permite executar código em resposta a mudanças específicas nas dependências, neste caso, a dependência é [nome]. Isso significa que o código dentro deste bloco será acionado sempre que o valor da variável nome mudar.
Dentro do useEffect, é definida uma função assíncrona chamada buscaPokemon(). Esta função será responsável por fazer a requisição à API do Pokémon e obter os dados desse Pokémon. Após obter a resposta da API, os dados são extraídos do formato JSON e armazenados na variável data.
Um novo objeto chamado Pokemon é criado, e ele contém as informações do Pokémon que foram obtidas da API. Este objeto tem as propriedades nome e imagem, que representam o nome do Pokémon e a URL de sua imagem. Usamos a função setPokemon para atualizar o estado do componente com o objeto Pokemon criado.
Isso fará com que os dados do Pokémon sejam renderizados na interface do usuário. Antes de chamar a função buscaPokemon(), verificamos se o nome não está vazio, garantindo que a busca só seja realizada quando um nome válido for fornecido, evitando requisições desnecessárias à API.
return (
<div className="Pokemon">
<>
{Pokemon ? (
<>
<h1>{Pokemon.nome}</h1>
<img src={Pokemon.imagem} alt={Pokemon.nome} />
</>
) : (
<p>Carregando...</p>
)}
</>
</div>
);
}
export default Pokemon;
Aqui fizemos a parte da renderização do componente Pokémon e exibição das informações do Pokémon na interface do usuário com base nos dados obtidos da API. O resultado é o seguinte:
useContext
O useContext é um hook do React que permite acessar os valores de um contexto. Vamos simular um contexto relacionado ao mundo Pokémon, onde temos informações sobre o treinador de Pokémon e assim analisar como o useContext é utilizado. Primeiro, crie um arquivo para o contexto, por exemplo, PokemonContext.tsx:
import { createContext, useContext, useState, ReactNode } from 'react';
// Interface para representar o contexto
interface PokemonContextType {
treinador: string;
setTreinador: React.Dispatch<React.SetStateAction<string>>;
}
// Criar o contexto
const PokemonContext = createContext<PokemonContextType | undefined>(undefined);
// Provedor do contexto
interface PokemonProviderProps {
children: ReactNode;
}
export function PokemonProvider({ children }: PokemonProviderProps) {
const [treinador, setTreinador] = useState('Ash Ketchum');
return (
<PokemonContext.Provider value={{ treinador, setTreinador }}>
{children}
</PokemonContext.Provider>
);
}
// Hook personalizado para usar o contexto
export function usePokemon() {
const context = useContext(PokemonContext);
if (!context) {
throw new Error('usePokemon deve ser usado dentro de um PokemonProvider');
}
return context;
}
Neste exemplo TypeScript, criamos um contexto usando createContext. Este contexto é onde os dados serão armazenados e compartilhados entre os componentes. No nosso caso, o contexto é chamado de PokemonContext. O PokemonContext é criado com a tipagem PokemonContextType | undefined
. Também definimos uma interface PokemonContextType para representar o formato dos dados. Caso você queira saber mais sobre a criação de contextos, confira o curso React: gerenciamento de estados globais com ContextAPI.
Agora, vamos criar um componente que consome essas informações em TypeScript. Por exemplo, em um arquivo chamado TreinadorPokemon.tsx:
import React from 'react';
import { usePokemon } from './PokemonContext';
function TreinadorPokemon() {
const { treinador, setTreinador } = usePokemon();
return (
<div>
<h2>Treinador Pokémon:</h2>
<p>{treinador}</p>
<button onClick={() => setTreinador('Misty')}>Mudar Treinador</button>
</div>
);
}
export default TreinadorPokemon;
Aqui construímos a parte visual desse componente que consiste em um container com o título treinador pokemon, o nome dele e um botão que utiliza do setTreinador
para atualizar o nome em todos os componentes que utilizem esse contexto.
Certifique-se de envolver seu componente principal com o PokemonProvider
para que o contexto seja disponibilizado para os componentes que o consomem:
<PokemonProvider>
<TreinadorPokemon />
</PokemonProvider>
O resultado em sua tela será o seguinte:
Observe que podemos acessar treinador
e setTreinador
diretamente no TreinadorPokemon
. Quando o botão "Mudar Treinador" é clicado, a função setTreinador
é chamada, e o valor do contexto é atualizado, o que, por sua vez, atualiza todos os componentes que consomem o contexto.
Em resumo, o useContext
permite que você acesse os valores de um contexto em componentes específicos, sem a necessidade de passar dados através de props manualmente. Isso simplifica a comunicação entre componentes e é particularmente útil quando você precisa compartilhar dados ou estados em vários níveis da árvore de componentes.
Custom hooks
Custom hooks são funções que seguem a convenção de começar com use e podem usar outros hooks dentro delas. Eles permitem que você extraia a lógica do estado de um componente e a reutilize em outros componentes. Um exemplo de custom hooks é o usePokemon
, criado no exemplo anterior.
Para nos aprofundarmos ainda mais no assunto, vamos criar um novo custom hook chamado usePokebola, em um novo arquivo chamado usePokebola.tsx:
import { useState } from 'react';
type PokebolaState = 'aberta' | 'fechada';
Primeiramente definimos o tipo para representar o estado da Pokébola, que é as strings 'aberta' ou 'fechada'. Isso significa que uma variável ou parâmetro do tipo PokebolaState só pode ter um desses dois valores como seu valor possível. Ou seja, uma variável do tipo PokebolaState pode ser igual a 'aberta' ou 'fechada', e qualquer outro valor não é permitido.
function usePokebola(): [PokebolaState, () => void, () => void] {
// Use useState para gerenciar o estado da Pokébola
const [estadoPokebola, setEstadoPokebola] = useState<PokebolaState>('fechada');
// Função para abrir a Pokébola
const abrirPokebola = () => {
setEstadoPokebola('aberta');
};
// Função para fechar a Pokébola
const fecharPokebola = () => {
setEstadoPokebola('fechada');
};
return [estadoPokebola, abrirPokebola, fecharPokebola];
}
export default usePokebola;
Agora, partindo para o custom hook usePokebola em si, ele é uma função que não recebe nenhum argumento e específica que ela retorna uma tupla contendo três elementos. A primeira posição da tupla é do tipo PokebolaState, que representa o estado da Pokébola (aberta ou fechada). As duas próximas posições são funções que não recebem argumentos e não retornam nada.
Dentro deste hook, utilizamos o hook useState para criar uma variável de estado chamada estadoPokebola que é inicializada com o valor 'fechada'. O tipo do estado é especificado como PokebolaState, garantindo que ele só possa conter os valores 'aberta' ou 'fechada'.
A seguir, definimos duas funções, a abrirPokebola e a fecharPokebola, ambas servem para alterar o estado da pokebola. Por fim, a função usePokebola retorna uma tupla contendo o estado atual da Pokébola (estadoPokebola) e as funções para abrir (abrirPokebola) e fechar (fecharPokebola) a Pokébola.
Utilizando este custom hook em um componente React, você pode facilmente controlar o estado da Pokébola em sua aplicação, alternando entre os estados 'aberta' e 'fechada' conforme necessário. Vamos usar esse custom hook em um componente React?
import usePokebola from '../usePokebola';
import PokebolaAberta from "./pokebolaaberta.png";
import PokebolaFechada from "./pokebolafechada.png";
function Pokebola() {
// Use o hook usePokebola
const [estadoPokebola, abrirPokebola, fecharPokebola] = usePokebola();
return (
<div>
<img src={estadoPokebola == "aberta"
? PokebolaAberta
: PokebolaFechada} alt={`Pokebola ${estadoPokebola}`} />
<div>
<button onClick={abrirPokebola}>Abrir Pokébola</button>
<button onClick={fecharPokebola}>Fechar Pokébola</button>
</div>
</div>
);
}
export default Pokebola;
Por fim construímos a parte visual desse componente que consiste em um container com um elemento de imagem que de acordo com o estadoPokebola renderiza a foto de uma Pokebola aberta ou fechada. Também há botões para abrir ou fechar a Pokebola, resultando em:
Você pode criar seus próprios custom hooks para reutilizar a lógica do estado entre os componentes que precisam dela. Também pode usar os custom hooks de outras pessoas, como os disponíveis na biblioteca useHooks.
Hooks com tipos dinâmicos
Tipos dinâmicos em hooks são tipos que não são definidos de forma explícita, mas são inferidos pelo React a partir dos valores que são passados ou retornados pelos hooks.
Por exemplo, se você usar o hook useState
para criar um estado que armazena um número, o React vai inferir que o tipo desse estado é number
, mesmo que você não tenha especificado isso.
Da mesma forma, se você usar o hook useContext
para acessar um contexto que provê um objeto com propriedades name
e age
, o React vai inferir que o tipo desse contexto é { name: string, age: number }
, mesmo que você não tenha definido isso.
Veja um exemplo de código que usa tipos dinâmicos em hooks:
import { useState } from 'react';
function PokemonLevel() {
const [pokemonLevel, setPokemonLevel] = useState(21);
O componente utiliza o hook useState para criar um estado chamado pokemonLevel e uma função setPokemonLevel para atualizar esse estado. O estado é inicializado com o valor 21, que é o nível inicial do Pokémon Ninetales.
Esse estado é um exemplo de tipo dinâmico, pois não especificamos seu tipo explicitamente. Em vez disso, TypeScript infere o tipo com base no valor inicial que é 21. Isso significa que TypeScript determina que pokemonLevel
é um número (number) com base no valor atribuído inicialmente.
const aumentarNivel = () => {
setPokemonLevel(pokemonLevel + 1);
};
const diminuirNivel = () => {
if (pokemonLevel > 0) {
setPokemonLevel(pokemonLevel - 1);
}
};
O componente possui duas funções:
aumentarNivel: Ela utiliza a função setPokemonLevel para aumentar o nível do Pokémon em 1 unidade. O nível só pode ser aumentado se já for maior que 0.
diminuirNivel: Ela utiliza a função setPokemonLevel para diminuir o nível do Pokémon em 1 unidade, desde que o nível atual seja maior que 0.
return (
<div>
<h1>Ninetales</h1>
<img src="https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/38.png" alt="Imagem do pokemon ninetales" />
<p>Nível atual do Pokémon: {pokemonLevel}</p>
<button onClick={aumentarNivel}>Aumentar Nível</button>
<button onClick={diminuirNivel}>Diminuir Nível</button>
</div>
);
}
export default PokemonLevel;
Na parte de renderização do componente, exibimos um titulo com o nome ‘Ninetales’ e uma imagem desse Pokémon. Também temos um parágrafo que exibe o nível atual com base no valor do estado pokemonLevel
e dois botões <button>
, um para aumentar o nível e outro para diminuir o nível do Pokémon. Eles são associados às funções aumentarNivel
e diminuirNivel
, respectivamente, resultando no seguinte componente:
Apesar disso, o uso de tipos dinâmicos pode causar diversos problemas, como:
- Maior probabilidade de erros: Sem verificação de tipos, você está mais propenso a cometer erros relacionados a tipos, como passar argumentos incorretos para funções ou usar propriedades inexistentes em objetos.
- Redução do benefício da tipagem estática: Uma das vantagens do TypeScript é a tipagem estática, que ajuda a detectar erros de tipo em tempo de compilação. O uso de tipos dinâmicos reduz esse benefício, tornando o TypeScript mais semelhante ao JavaScript padrão.
- Manutenção difícil: À medida que o código cresce e evolui, a falta de tipos explícitos torna mais difícil entender como diferentes partes do código se relacionam e interagem.
Para evitar esses problemas, é recomendável tipar os hooks de forma explícita. Isso pode te ajudar a evitar erros de tipo e melhorar a legibilidade do código.
Conclusão
Assim como os treinadores se dedicam a treinar e fortalecer seus Pokémon, você, como pessoa desenvolvedora, aprimorou suas habilidades ao compreender a como especificar tipos para os seus hooks, protegendo seu código contra erros e facilitando a colaboração com outros membros da equipe.
Da mesma forma que os Pokémon evoluem e se tornam mais poderosos, seu conhecimento em TypeScript e React se expandiu, permitindo que você crie aplicativos mais robustos e eficazes.
Lembre-se de que, assim como cada Pokémon tem suas próprias características e pontos fortes, cada projeto React pode ter necessidades de tipagem únicas. Para embarcar nesta jornada de aprimoramento em React com TypeScript, você pode confiar no Tech Guide como seu guia, assim como um Treinador Pokémon confia em seu Pokédex.
Com dedicação e práticas contínuas você estará pronto para enfrentar todas as batalhas e desafios de desenvolvimento que surgirem em seu caminho! Nos vemos por aí! :)