Skip to main content

ReactJS Hooks - use Experimental Hook

· 10 min read
Bruno Carneiro
Fundador da @TautornTech

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.

Projeto final

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 o await, o use 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.

Texto completo

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.

Promise Current version
Promise Current Version

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:

Current Promise version
Promise Experimental Version

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 e rejeitado
  • 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