ReactJS Hooks - use Experimental Hook
ReactJS Hooks use [Experimental]
Olá jovens gafanhotos, seguindo a trilha de artigos a respeito dos Hooks, neste artigo irei falar sobre o use
. Um Hook que está em fase experimental e deve estar disponível em breve no React.
Esse hook pode ajudar bastante o nosso dia-a-dia porque atualmente a quantidade de código escrito para resolver uma promise é bem grande. Mas não apenas isso.
E para demonstrar como o hook funciona realizando uma comparação com o estado atual eu criei um projeto simples demonstrando algunas cartas do jogo Hearthstone.
Caso queira saber um pouco mais sobre os Hooks, você pode ler este conteúdo aqui: React Hooks, Introdução
Conheça também como funciona o hook useEffect
O que é RFC?
Mas antes de iniciarmos os exemplos preciso explicar o que é RFC.
RFC significa Request for Comments, ou seja, existe uma solitação para comentários sobre um determinado assunto. Uma RFC é um documento técnico onde várias pessoas podem discutir sobre uma determinada tecnologia, métodos, pesquisas, ideias e etc. No caso do React existe uma RFC aberta discutindo a respeito do suporte de First Class Support para Promises e métodos com async/await.
Essa RFC traz uma versão experimental do React para que a comunidade possa utilizar o suporte de resolução de uma promise javascript utilizando o Suspense
do React. Algo que já é aguardado há algum tempo.
O que a RFC de adição de First Class Support irá trazer:
- Suporte para resolução de async/await em componentes do lado do servidor. Escrita de components do lado do Servidor utilizando a sintaxe JavaScript
await
definindo o seu componente como uma função assíncrono. - Introdução do hook
use
. Assim como oawait
, ouse
vai ser utilizado com promises, podendo ser utilizando normalmente com components e Hooks, até mesmo do lado do Client.
Isso permite que desenvolvedores React acessem dados de requisições assíncronas com o a api Suspense
do React.
Importante, como essa versão ainda está em modo experimental e existe uma discução a respeito dela então mudanças podem surgir, então aguarde a versão final antes de utilizá-la em produção :)
Hands on
Agora vou demonstrar algumas vantagens que o hook use
poderá trazer no desenvolvimento com React.
Utilizei o Vite para criar a estrutura inicial do projeto, para isso excute os comandos abaixos:
$ yarn create vite
Informe o nome do projeto ou tecle enter (chamei de reactjs-use-hook)
- ReactJS
- TypeScript
// Utilizando Yarn
$ cd reactjs-use-hook
$ yarn add react@experimental react-dom@experimental
ou
// Utilizando NPM
$ cd reactjs-use-hook
$ npm install react@experimental react-dom@experimental
Abra o arquivo tsconfig.json e adicione a opção "types": ["react/experimental"]
para que o TypeScript reconheça o novo Hook.
// tsconfig.json
{
"compilerOptions": {
// outras opções
"types": ["react/experimental"]
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
Para o desenvolvimento deste projeto utilizei uma api pública do Hearthstone.
Agora basta executar o comando abaixo para iniciar o projeto:
$ yarn dev
Logo, para fins de comparação, vou criar duas versões deste projeto, a primeira utilizando useEffect
e useState
(CurrentVersion.tsx) para controlar toda a chamada assíncrona e a segunda com hook use
(ExperimentalVersion.tsx).
Estrutura de como ficou o projeto final:
Não adicionarei neste tutorial a estrutura de arquivos .css
e os assets
utilizados.
Os arquivos estão disponíveis neste repositório reactjs-use-hook caso queira reproduzir o projeto por inteiro.
Para iniciar crie um arquivo .env
na raíz da pasta contendo as chaves da api do HearthStone.
VITE_RAPID_API_KEY=sua-api-key
VITE_RAPID_API_HOST=sua-api-host
Obs.: Não utilizei as minhas chaves que estão no repositório porque elas possuem limite de uso diário e/ou podem ficar inválidas. Então recomendo fortemente que para reprodução deste projeto utilize as suas próprias chaves.
Logo após isso modifique o arquivo App.tsx, ficando desta maneira:
// App.tsx
import { useState } from "react"
import logo from './assets/logo.png'
import ExperimentalVersion from './ExperimentalVersion'
import CurrentVersion from './CurrentVersion'
import './App.css'
function App() {
const [version, setVersion] = useState<string>('experimental') // escolha da versão
const [cardClass, setCardClass] = useState<string>('warlock') // escolha da classe das cartas
return (
<div className="App">
<div>
<a href="https://tautorn.com.br" target="_blank">
<img src={logo} className="logo tautorn tech" alt="Tautorn Tech Logo" title="Tautorn Tech Logo" />
</a>
<div>
<button onClick={() => setVersion('current')}>Current Version</button>
<button onClick={() => setVersion('experimental')}>Experimental Version</button>
</div>
<div style={{ margin: '2rem'}}> <!-- Não criei classe apenas por comodismo deste exemplo, mas não crie css inline-->
<button onClick={() => setCardClass('warlock')}>Warlock</button>
<button onClick={() => setCardClass('druid')}>Druid</button>
<button onClick={() => setCardClass('mage')}>Mage</button>
<button onClick={() => setCardClass('warrior')}>Warrior</button>
</div>
{version === 'current' && (<CurrentVersion cardClass={cardClass} />)}
{version === 'experimental' && (<ExperimentalVersion cardClass={cardClass} />)}
</div>
</div>
)
}
export default App
No arquivo acima utilizo duas importações import ExperimentalVersion from './ExperimentalVersion'
e import CurrentVersion from './CurrentVersion'
que são as duas versões que estão em comparação.
Criei também a possibilidade de variar entre cada versão e também poder chamar diferentes tipos de classes do Hearthstone para realizar requições assíncronas diferentes.
Alternância entre versões:
<button onClick={() => setVersion('current')}>Current Version</button>
<button onClick={() => setVersion('experimental')}>Experimental Version</button>
Apresentação das versões de acordo com o que foi escolhido utilizando o setVersion
{version === 'current' && (<CurrentVersion cardClass={cardClass} />)}
{version === 'experimental' && (<ExperimentalVersion cardClass={cardClass} />)}
Agora crie os arquivos CurrentVersion.tsx e Experimental.tsx
CurrentVersion
CurrentVersion.tsx
import { useEffect, useState } from 'react'
interface CurrentVersionProps {
cardClass: string
}
function CurrentVersion({ cardClass }: CurrentVersionProps) {
const [data, setData] = useState([])
useEffect(() => {
const fetchCards = async (id: string) => {
const response = await fetch(`https://omgvamp-hearthstone-v1.p.rapidapi.com/cards/classes/${id}`, {
headers: {
'X-RapidAPI-Key': `${import.meta.env.VITE_RAPID_API_KEY}`,
'X-RapidAPI-Host': `${import.meta.env.VITE_RAPID_API_HOST}`
}
})
const dataResponse = await response.json()
// O filter foi adicionado para para filtrar cards que possuam o atributo `img`, do contrário vários cards ficarão "quebrados" durante a renderização da página.
// O slice é para poder retornar somente 20 cards para não ficar uma apresentação muito grande. Isso foi necessário uma vez que no momento da criação deste artigo a api não dispõe da possibilidade informar a quantidade de cartas para retornar.
setData((dataResponse.filter((card: any) => card.img)).slice(0, 19))
}
fetchCards(cardClass)
.catch(console.error)
}, [cardClass])
const renderClasses = (card: any, key: number) => {
return (
<img key={key} src={card.img} alt={card.name} height="256px" />
)
}
return (
<div>
<h2>Current Version</h2>
{data?.map(renderClasses)}
</div>
)
}
export default CurrentVersion
Para a Current Version faço uma chamada utilizando a api JavaScript fetch
dentro do hook useEffect através do método fetchCards()
informando o tipo da classe (este é selecionado no arquivo App.tsx, na escolha da classe).
Dessa forma, assim que a página é renderizada a chamada dos cards é realizada e logo após a conclusão e tratamento dos dados (filter e slice) os valores são "setados" através do hook setData.
Aqui faço uma pequena verificação com o Optional Chaining para apresentar os dados somente quando a variável data estiver preenchida
{data?.map(renderClasses)}
Essa abordagem exige dois hooks e sempre que realizo uma alternância entre as classes uma nova requisição vai ser realizada. Esse "monitoramento" é feito através do useEffect "ouvindo" o parâmetro [cardClass]
Além de sempre realizar uma nova requisição utilizar o useEffect
pode acabar disparando N chamadas de acordo com a renderização da página. Inicialmente serão duas (isso pode ser contornado mas não realizei essa alteração neste projeto, de todo jeito é mais um passo manual).
Repare que na imagem abaixo sempre que altero entre cada classe é realizado uma nova requisição, independente se a chamada já havia sido realizada.
Com o use
essa abordagem pode ser diferente, vou explicar mais para frente.
ExperimentalVersion
// Experimental.tsx
import { cache, use, Suspense } from "react"
interface CardsProps {
id: string
}
interface ExperimentalVersionProps {
cardClass: string
}
const fetchCards = cache(async (id: string) => {
const response = await fetch(`https://omgvamp-hearthstone-v1.p.rapidapi.com/cards/classes/${id}`, {
headers: {
'X-RapidAPI-Key': `${import.meta.env.VITE_RAPID_API_KEY}`,
'X-RapidAPI-Host': `${import.meta.env.VITE_RAPID_API_HOST}`
}
})
const dataResponse = await response.json()
// O filter foi adicionado para para filtrar cards que possuam o atributo `img`, do contrário vários cards ficarão "quebrados" durante a renderização da página.
// O slice é para poder retornar somente 20 cards para não ficar uma apresentação muito grande. Isso foi necessário uma vez que no momento da criação deste artigo a api não dispõe da possibilidade informar a quantidade de cartas para retornar.
return (dataResponse.filter((card: any) => card.img)).slice(0, 19)
})
function Cards({ id }: CardsProps) {
const cards = use(fetchCards(id))
const renderClasses = (card: any, key: number) => {
return (
<img key={key} src={card.img} alt={card.name} height="256px" />
)
}
return (
<div>
{cards?.map(renderClasses)}
</div>
)
}
function ExperimentalVersion({ cardClass }: ExperimentalVersionProps) {
return (
<div>
<h2>Experimental Version</h2>
<Suspense fallback={<h2>Loading…</h2>}>
<Cards id={cardClass} />
</Suspense>
</div>
)
}
export default ExperimentalVersion
A abordagem com o hook use
começa a mudar; o primeiro ponto que descrevi neste artigo é o suporte a First Class Support, com isso o método para buscar os cards estão fora da função ExperimentalVersion, podendo assim realizar uma chamada e ler o conteúdo através uma promise com a api Suspense
Promise
const fetchCards = cache(async (id: string) => {
const response = await fetch(`https://omgvamp-hearthstone-v1.p.rapidapi.com/cards/classes/${id}`, {
// something
})
Suspense
<Suspense fallback={<h2>Loading…</h2>}>
<Cards id={cardClass} />
</Suspense>
Reparem que também adicionei um wrapper chamado cache
na função fetchCards
cache(async (id: string) => {
Dessa maneira é realizado um "cacheamento" da requisição, com isso sempre que a mesma requisição é realizada o que estiver no cache vai ser retornando sem a necessidade de uma nova requisição na api, diferente do que acontece com a abordagem sem o wrapper de cache, conforme imagem abaixo:
A utilização do cache
não é obrigatória - por ora - mas é altamente recomendada pelo time de acordo com o que está na RFC. De todo jeito a utilização do cache pode sofrer várias abordagens, como necessidade de invalidação do mesmo ou mudanças de rota.
Mesmo assim a utilização do cache não substitui bibliotecas como react query do qual possui vários outros recursos e não apenas cacheamento.
Conclusão
Como pode ser observado a utilização do novo hook reduz drasticamente a lógica e quantidade de hooks, tornando o código mais simples. Além da possibilidade de combinação da api cache
tornando as chamadas assíncronas muito mais eficientes.
Apesar de não ter sido apresentado aqui é muito importante lembrar que essa aplicação também pode ser realizada com Server Components.
Alguns pontos ainda estão em abertos pela comunidade, como:
- Como serão os tratamentos de erro?
- Se adicionarão o controle de estado para
pendente
erejeitado
- Algumas pessoas são contras essa abordagem porque o ReactJS é uma biblioteca e não um framework.
- A api de cache ainda será adicionada em uma RFC complementar porque ela precisa ser melhor abordada.
Por ora é isso, lembrando que essa NÃO é a versão final do hook use
e que a discussão ainda está aberta, então alterações podem surgir ao longo do tempo.
De todo jeito a comunidade está empenhanda em contribuir com essa possível melhoria e na minha opinião vai ser muito bem vinda porque ajuda bastante no desenvolvimento no dia-a-dia.
Espero que tenha gostado deste artigo e até o próximo ;)
Para visualizar o projeto completo acesse reactjs-use-hook
Referências
- https://github.com/reactjs/rfcs/pull/229
- https://github.com/acdlite/rfcs/blob/first-class-promises/text/0000-first-class-support-for-promises.md
- https://xebia.com/blog/use-hook-the-new-experimental-javascript-react-feature/
- https://www.youtube.com/watch?v=ytXM05PVcFU&ab_channel=JackHerrington
- https://vived.io/new-hook-is-coming-to-react-frontend-weekly-vol-109/
- https://17.reactjs.org/docs/concurrent-mode-suspense.html
- https://rapidapi.com/omgvamp/api/hearthstone