Consumindo APIs com o TanStack Query (React Query)

Consumindo APIs com o TanStack Query (React Query)

Olá leitor 🙂

Na lição de hoje nós iremos aprender a como profissionalizar o nível das nossas requisições HTTP usando a biblioteca React Query.

Na lição passada, você aprendeu a realizar Requisições HTTP com o ReactJS.

Onde viu a utilização de algumas funcionalidades e bibliotecas como:

  • XMLHttpRequest
  • Fetch API
  • Axios
  • E um módulo de HTTP usando Facades

Até então a nossa aplicação consegue realizar solicitações e enviar dados via método GET, POST, UPDATE e etc...

So que, em uma aplicação real, se você fizer exatamente do jeito que foi explicado na lição passada, dependendo da complexidade da sua aplicação, ela pode não se tornar tão escalável assim 🥲

Hoje, nós iremos ver alguns exemplos bem práticos do porque você deve fazer o uso da biblioteca TanStack Query (antiga React Query) em suas aplicações feitas com ReactJS.

Vamos nessa? 🙂

Criando seu projeto de testes

Antes de começarmos, vamos criar um novo projeto em ReactJS dedicado a esta lição. No meu caso criei um novo projeto chamado de requisicoes-react-query:  

npx create-react-app requisicoes-react-query

Após isso, não se esqueça de fazer aquela limpeza do código, e também aquela organizada nas pastas do src (organização por funcionalidade) 😁

Feito isso, nós vamos criar uma pequena aplicação que vai fazer uma requisição a uma API externa, de modo que possamos identificar alguns "lacks".

Lacks nada mais são do que pequenos problemas geralmente associados a performance da nossa aplicação, mas que a curto e médio prazo não costumam gerar problemas significativos.

Instalando o Axios

Nesta lição também faremos o uso do Axios, portanto:

npm install axios

E não se esqueça de trazer pra cá o nosso módulo que criamos na lição passada (src > services > httpService.js)

import axios from 'axios';

class HttpService {
 constructor(baseURL = 'http://localhost/api-roupas') {
 this.baseUrl = baseURL;
 this.instance = axios.create({ baseURL: this.baseUrl });
 }

 get defaultHeaders() {
 return {
 'Authorization': localStorage.getItem('Authorization'),
 'Content-Type': 'application/json',
 };
 }

 async request(method, url, data = null, customHeaders = {}) {
 const headers = { ...this.defaultHeaders, ...customHeaders };
 const source = axios.CancelToken.source();
 
 const config = {
 method,
 url,
 headers,
 cancelToken: source.token
 };
 
 if (data) {
 config.data = data;
 }
 
 try {
 const response = await this.instance(config);
 return response.data; // Retorna somente os dados da resposta
 } catch (error) {
 throw error; // Rejeita a promessa com o erro
 } finally {
 source.cancel();
 }
 } 

 get(url, customHeaders = {}) {
 return this.request('get', url, null, customHeaders);
 }
 
 post(url, data, customHeaders = {}) {
 return this.request('post', url, data, customHeaders);
 }
 
 put(url, data, customHeaders = {}) {
 return this.request('put', url, data, customHeaders);
 }
 
 delete(url, customHeaders = {}) {
 return this.request('delete', url, null, customHeaders);
 }

 setBaseUrl(newUrl) {
 this.baseUrl = newUrl;
 this.instance.defaults.baseURL = newUrl;
 }
}

export default HttpService;

Lista de Roupas

Vamos criar um pequeno componente chamado de ListaDeRoupas que vai ficar responsável por trazer do nosso servidor algumas peças que estão em promoção.

ListaDeRoupas > index.jsx:

import React, {useState, useEffect} from 'react';
import './lista-de-roupas.css';
import HttpService from '../../services/httpService';

const httpService = new HttpService();

const ListaDeRoupas = () => {
 const [roupas, setRoupas] = useState([]);

 const fetchRoupas = async () => {
 httpService.get('/getRoupas.php').then(data => {
 setRoupas(data);
 }).catch(error => {
 console.log('Houve um erro na aplicação', error);
 });
 
 }

 useEffect(() => {
 fetchRoupas();
 }, []);

 return(
 <div className="lista-de-roupas">
 <h2>📢 Roupas em Promoção 🛒</h2>
 <div className="roupas">
 {roupas.map((roupa, index) => (
 <div className="roupa" key={index}>
 <img src={roupa.img} alt={"Roupa " + (index + 1)} />
 <p>{roupa.nome}</p>
 <p>{roupa.preco}</p>
 </div>
 ))}
 </div>
 </div>
 )
}

export default ListaDeRoupas;

ListaDeRoupas > lista-de-roupas.css:

*{
 margin: 0;
 padding: 0;
 box-sizing: border-box;
}

.lista-de-roupas{
 width:100%;
 height: 100vh;
 background-color: #5f27cd;
}

.lista-de-roupas h2{
 width:100%;
 font-family: 'Arial Black', Courier, monospace;
 font-size: 28px;
 color: white;
 text-align: center;
 padding-top:40px;
}

.lista-de-roupas .roupas{
 display: flex;
 justify-content: center;
 align-items: center;
 margin-top:60px;
}

.lista-de-roupas .roupas .roupa{
 background-color: white;
 border-radius:12px;
 margin:15px;
 padding: 12px;
 text-align: center;
}

.lista-de-roupas .roupas .roupa p:nth-child(2){
 font-family: 'Arial Black', Courier, monospace;
 font-size: 16px;
 color: #1dd1a1;
 margin-bottom:8px;
}

Não se esqueça de adicionar o seu componente dentro do App.js:

import ListaDeRoupas from "./components/ListaDeRoupas";

export default function App() {
 return (
 <>
 <ListaDeRoupas />
 </>
 );
}

Para quesitos exemplares, eu simulei a resposta de uma API com a linguagem PHP, aqui está o código:

<?php

header('Access-Control-Allow-Origin: *');

header('Access-Control-Allow-Methods: GET, POST');

header("Access-Control-Allow-Headers: X-Requested-With");

//Simulando um retorno de uma API de Roupas

$array = array(
	array('id' => 123, 'nome' => 'Camiseta Curta', 'preco' => 'R$ 50,00', 'img' => 'http://localhost/api-roupas/camiseta-curta.png'),
	array('id' => 887, 'nome' => 'Short Jeans', 'preco' => 'R$ 43,00', 'img' => 'http://localhost/api-roupas/short-jeans.png'),
	array('id' => 52, 'nome' => 'Calçado de Praia', 'preco' => 'R$ 22,00', 'img' => 'http://localhost/api-roupas/calcado-de-praia.png'),
	array('id' => 12, 'nome' => 'Toca de Frio', 'preco' => 'R$ 12,00', 'img' => 'http://localhost/api-roupas/toca-de-frio.png'),
);

echo json_encode($array);

As imagens utilizadas podem ser encontradas dentro da pasta src > assets na aplicação final que está no nosso repositório do GitHub 😁

Perfeito, no código acima nós temos um estado (roupas) que controla o array de roupas que é retornado pela função fetchRoupas, e que está sendo chamado logo após a nossa aplicação ser renderizada na UI (useEffect).

O código acima funciona perfeitamente, mas vamos supor que eu queria incrementar ainda mais o nosso componente.

Vamos supor que estamos trabalhando com um servidor SUPER LENTO, e que demora bastante para retornar as peças de roupas que estão em promoção.

O que você faria? Deixaria a tela em branco fazendo com que o seu usuário tenha que esperar uma eternidade?

É obvio que não!

Você poderia criar mais um estado chamado de isLoading com o valor inicial de true. E conectá-lo na sua UI de modo a informar o usuário que tem algo sendo carregado:

ListaDeRoupas >index.jsx:  

import React, {useState, useEffect} from 'react';
import './lista-de-roupas.css';
import HttpService from '../../services/httpService';

const httpService = new HttpService();

const ListaDeRoupas = () => {
 const [roupas, setRoupas] = useState([]);
 const [isLoading, setIsLoading] = useState(true);

 const fetchRoupas = async () => {
 httpService.get('/getRoupas.php').then(data => {
 setRoupas(data);
 setIsLoading(false);
 }).catch(error => {
 console.log('Houve um erro na aplicação', error);
 });
 
 }

 useEffect(() => {
 fetchRoupas();
 }, []);

 return(
 <div className="lista-de-roupas">
 <h2>📢 Roupas em Promoção 🛒</h2>
 {isLoading ? (
 <p style={{textAlign: 'center', color: 'white'}}>Carregando...</p>
 ) : (
 <div className="roupas">
 {roupas.map((roupa, index) => (
 <div className="roupa" key={index}>
 <img src={roupa.img} alt={"Roupa " + (index + 1)} />
 <p>{roupa.nome}</p>
 <p>{roupa.preco}</p>
 </div>
 ))}
 </div>
 )}
 </div>
 )
}

export default ListaDeRoupas;

Como estou usando o PHP, executei um sleep para simular uma possível lentidão:

<?php

header('Access-Control-Allow-Origin: *');

header('Access-Control-Allow-Methods: GET, POST');

header("Access-Control-Allow-Headers: X-Requested-With");

//Simulando um retorno de uma API de Roupas

$array = array(
	array('id' => 123, 'nome' => 'Camiseta Curta', 'preco' => 'R$ 50,00', 'img' => 'http://localhost/api-roupas/camiseta-curta.png'),
	array('id' => 887, 'nome' => 'Short Jeans', 'preco' => 'R$ 43,00', 'img' => 'http://localhost/api-roupas/short-jeans.png'),
	array('id' => 52, 'nome' => 'Calçado de Praia', 'preco' => 'R$ 22,00', 'img' => 'http://localhost/api-roupas/calcado-de-praia.png'),
	array('id' => 12, 'nome' => 'Toca de Frio', 'preco' => 'R$ 12,00', 'img' => 'http://localhost/api-roupas/toca-de-frio.png'),
);

sleep(3);//O servidor só vai retornar uma resposta depois de 3 segundos

echo json_encode($array);

Veja como ficou o resultado final:

Legal, até aí tudo bem... apesar dessa estratégia ficar legal, o nosso Loading (estado) não ficaria 100% fiel ao tempo de Loading que a API realmente está levando.

Agora, vamos supor que o servidor está apresentando muitas falhas ultimamente, e que precisamos contornar isso também no Front-End.

Para resolver isso, você pode setar um novo estado chamado de IsError e acioná-lo dentro do catch da seguinte forma:

ListaDeRoupas > index.jsx:

import React, {useState, useEffect} from 'react';
import './lista-de-roupas.css';
import HttpService from '../../services/httpService';

const httpService = new HttpService();

const ListaDeRoupas = () => {
 const [roupas, setRoupas] = useState([]);
 const [isLoading, setIsLoading] = useState(true);
 const [isError, setIsError] = useState(false);

 const fetchRoupas = async () => {
 httpService.get('/getRoupas.php').then(data => {
 setRoupas(data);
 setIsLoading(false);
 }).catch(error => {
 console.log('Houve um erro na aplicação', error);
 setIsError(true);
 setIsLoading(false);
 });
 
 }

 useEffect(() => {
 fetchRoupas();
 }, []);

 return(
 <div className="lista-de-roupas">
 <h2>📢 Roupas em Promoção 🛒</h2>
 {isLoading ? (
 <p style={{textAlign: 'center', color: 'white'}}>Carregando...</p>
 ) : isError ? (
 <p style={{textAlign: 'center', color: 'red'}}>Houve um erro ao carregar os dados das roupas. Por favor, tente novamente mais tarde.</p>
 ) : (
 <div className="roupas">
 {roupas.map((roupa, index) => (
 <div className="roupa" key={index}>
 <img src={roupa.img} alt={"Roupa " + (index + 1)} />
 <p>{roupa.nome}</p>
 <p>{roupa.preco}</p>
 </div>
 ))}
 </div>
 )}
 </div>
 )
}

export default ListaDeRoupas;

Do lado do PHP, vamos simular um erro retornando um status code de 500:

<?php

header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST');
header("Access-Control-Allow-Headers: X-Requested-With");

// Simulando um retorno de uma API de Roupas

$array = array(
	array('id' => 123, 'nome' => 'Camiseta Curta', 'preco' => 'R$ 50,00', 'img' => 'http://localhost/api-roupas/camiseta-curta.png'),
	array('id' => 887, 'nome' => 'Short Jeans', 'preco' => 'R$ 43,00', 'img' => 'http://localhost/api-roupas/short-jeans.png'),
	array('id' => 52, 'nome' => 'Calçado de Praia', 'preco' => 'R$ 22,00', 'img' => 'http://localhost/api-roupas/calcado-de-praia.png'),
	array('id' => 12, 'nome' => 'Toca de Frio', 'preco' => 'R$ 12,00', 'img' => 'http://localhost/api-roupas/toca-de-frio.png'),
);

// Adicionando um atraso de 3 segundos
sleep(3);

// Simulando um erro de servidor
http_response_code(500);
echo "Erro interno do servidor";

Veja como ficou o resultado final:

Vemos que essa estratégia funciona perfeitamente também, só que isso pode te gerar um problema futuro, uma vez que pode você pode se pegar criando muitos estados auxiliares dentro do seu componente. 

Um outro exemplo envolveria um estado para cancelar a requisição caso ela demorasse muito, uma funcionalidade para consumir um outro endpoint caso o atual não desse certo, ou quem sabe uma funcionalidade que vai ficar de 12 em 12 minutos consumindo os dados da API em busca de atualizações caso o seu usuário ainda esteja com a página aberta (famoso reFetch).

Ou seja, cada vez mais o nosso componente vai ficando mais complexo, mas será que haveria uma forma mais inteligente de se resolver isso?

Sim, existe! E podemos fazer isso facilmente usando a biblioteca TanStack Query (antiga React Query) 😎

Mas antes de chegarmos nela, vamos dar uma segurada nos ânimos e analisar um outro exemplo que envolve um Batch de Promises em uma aplicação em ReactJS.

Consumindo a mesma API em componentes separados

Vamos criar agora 2 componentes chamados de Nome e Curriculo, e uma página chamada de Usuario (existente dentro de src > pages), que por sua vez, irá fazer o uso dos dois componentes que criamos (Nome e Curriculo).

Nome > index.jsx:

import React, {useEffect, useState} from 'react';
import HttpService from '../../services/httpService';

const httpService = new HttpService();

const Nome = () => {
 const [name, setName] = useState('');

 const fetchNome = async () => {
 httpService.get('/getUserInfo.php').then(data => {
 setName(data.name);
 }).catch(error => {
 console.log('Houve um erro na aplicação', error);
 });
 
 }
 
 useEffect(() => {
 fetchNome();
 }, []);
 
 return (
 <div>
 <h3>Nome: {name}</h3>
 </div>
 );
}

export default Nome;

Curriculo > index.jsx:

import React, {useEffect, useState} from 'react';
import HttpService from '../../services/httpService';

const httpService = new HttpService();

const Curriculo = () => {
 const [curriculo, setCurriculo] = useState('');
 
 const fetchCurriculo = async () => {
 httpService.get('/getUserInfo.php').then(data => {
 setCurriculo(data.curriculo);
 }).catch(error => {
 console.log('Houve um erro na aplicação', error);
 });
 
 }
 
 useEffect(() => {
 fetchCurriculo();
 }, []);
 
 return (
 <div>
 <a href={curriculo} target="_blank">Download do Currículo</a>
 </div>
 );
}

export default Curriculo;

Usuario > index.jsx:

import Nome from '../../components/Nome';
import Curriculo from '../../components/Curriculo';

const Usuario = () => {
 return(
 <>
 <Nome />
 <Curriculo />
 </>
 )
}

export default Usuario;

Por fim, não se esqueça de adicionar a página Usuario dentro de App.js:

import Usuario from "./pages/Usuario";

export default function App() {
 return (
 <>
 <Usuario />
 </>
 );
}

Abaixo está o código PHP que criei para simular uma resposta da API:

<?php

header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST');
header("Access-Control-Allow-Headers: X-Requested-With");

// Simulando um retorno de uma API de Roupas

$array = array('name' => 'Micilini Roll', 'curriculo' => 'https://github.com/micilini');

echo json_encode($array);

O resultado final ficou bem simples, porém funcional:

Perfeito, entretanto, há um grande problema na lógica dessa aplicação:

  • Por que você está fazendo duas requisições para o mesmo endpoint em dois componentes diferentes?

Com a pergunta acima, nós podemos chegar a algumas soluções.

Solução 1) Faça chamadas na página Usuario e repasse o retorno para os componentes filhos.

A primeira solução, envolve deixar de fazer chamadas dentro dos nossos componentes Nome e Curriculo, optando por realizar apenas uma única chamada na página Pai (Usuario,), por exemplo:

Usuario > index.jsx:

import React, { useEffect, useState } from 'react';
import Nome from '../../components/Nome';
import Curriculo from '../../components/Curriculo';
import HttpService from '../../services/httpService';

const httpService = new HttpService();

const Usuario = () => {
 const [data, setData] = useState({name: '', curriculo: ''});

 const fetchData = async () => {
 httpService.get('/getUserInfo.php').then(data => {
 setData(data);
 }).catch(error => {
 console.log('Houve um erro na aplicação', error);
 }); 
 }

 useEffect(() => {
 fetchData();
 }, []);

 return(
 <>
 <Nome name={data.name}/>
 <Curriculo curriculo={data.curriculo} />
 </>
 )
}

export default Usuario;

Nome > index.jsx:

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

const Nome = ({ name }) => { 
 return (
 <div>
 <h3>Nome: {name}</h3>
 </div>
 );
}

export default Nome;

Curriculo > index.jsx:

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

const Curriculo = ({ curriculo }) => {
 return (
 <div>
 <a href={curriculo} target="_blank">Download do Currículo</a>
 </div>
 );
}

export default Curriculo;

Como resultado final ficou a mesma coisa, a única diferença é que nós estamos criando apenas uma única requisição para o nosso endpoint, ao invés de duas.

O que já melhora a performance da nossa aplicação 😁

Solução 2) Crie um contexto de forma a englobar a sua aplicação e distribuir os dados para os componentes que que precisam desses dados

Uma outra alternativa é criar um contexto acima da página Usuario e realizar a requisição por lá, de modo a disponibilizar esses dados para todos os componentes filho.

A forma como você pode fazer isso, está explicada de forma bastante detalhada na lição que fala sobre Criando Contextos com o hook useContext.

Exercício

Que tal completar a solução 2 deste tópico? Crie um contexto e faça uma requisição ao endpoint por lá, em seguida forneça esse contexto para o componente [Usuario] e repasse as informações necessárias para os componentes filhos [Nome] e [Curriculo]. 

Dito isto, agora veremos uma maneira totalmente nova de fazermos chamadas às nossas APIs 🙂

O que é o TanStack Query?

O TanStackQuery (antigo React Query pois antigamente só existia para ReactJS) nada mais é do que uma biblioteca bastante popular, que é utilizada para fazer o gerenciamento de estado e cache de dados em suas aplicações feitas com ReactJS.

De maneira simples e super eficiente, essa biblioteca busca armazanar em cache e atualizar dados que se comunicam de forma direta com APIs.

Basicamente, essa biblioteca veio para resolver os problemas de complexidade que podem surgir no nosso componente ListadeRoupas, de modo que consigamos resolver tais problemas de forma mais elegante (sem precisar criar milhares de estados).

Instalando o TanStackQuery

Para começarmos a usar a nossa biblioteca, vamos abrir o terminal da pasta raiz do projeto, e executar o comando de instalação:

npm i @tanstack/react-query

Após isso, vamos colocar a mão no código 😎

Implementando o TanStackQuery no ReactJS

Após a instalação, é recomendável que a biblioteca que acabamos de instalar envolva toda a nossa aplicação, e podemos fazer isso por meio do index.js.

Isso significa que essa biblioteca acaba atuando como um Provider, você ainda se lembra o que é um provider?

Dentro do arquivo index.js, precisamos encapsular o componente <App /> (que representa toda a nossa aplicação) usando o componente <QueryClientProvider /> da seguinte forma:

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { QueryClient , QueryClientProvider} from '@tanstack/react-query';

const client = new QueryClient();

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
 <React.StrictMode>
 <QueryClientProvider client={client}>
 <App />
 </QueryClientProvider>
 </React.StrictMode>
);

Observe que estamos instanciando a classe QueryClient dentro de uma constante chamada client, onde a mesma está sendo usada no atributo client do provedor QueryClientProvider.

Para mais informações do que está acontecendo por de baixo dos panos, não deixe de consultar a documentação da biblioteca.

Feito isso, a biblioteca está disponível e pronta para uso em todos os nossos componentes 😉

Usando o TanStackQuery no ReactJS

Para começar com o pé direito, vamos criar um novo componente chamado de ListaDeCalcados.

ListaDeCalcados > index.jsx:

import React, {useState, useEffect} from 'react';
import './lista-de-calcados.css';
import HttpService from '../../services/httpService';

const httpService = new HttpService();

const ListaDeCalcados = () => {
 const [calcados, setCalcados] = useState([]);
 const [isLoading, setIsLoading] = useState(true);
 const [isError, setIsError] = useState(false);

 const fetchCalcados = async () => {
 httpService.get('/getCalcados.php').then(data => {
 setCalcados(data);
 setIsLoading(false);
 }).catch(error => {
 console.log('Houve um erro na aplicação', error);
 setIsError(true);
 setIsLoading(false);
 });
 
 }

 useEffect(() => {
 fetchCalcados();
 }, []);

 return(
 <div className="lista-de-calcados">
 <h2>📢 Calçados do Verão 🛒</h2>
 {isLoading ? (
 <p style={{textAlign: 'center', color: 'white'}}>Carregando...</p>
 ) : isError ? (
 <p style={{textAlign: 'center', color: 'red'}}>Houve um erro ao carregar os dados dos calçados. Por favor, tente novamente mais tarde.</p>
 ) : (
 <div className="calcados">
 {calcados.map((calcado, index) => (
 <div className="calcado" key={index}>
 <img src={calcado.img} alt={"Calçado " + (index + 1)} style={{ width: '180px', height: 'auto' }} />
 <p>{calcado.nome}</p>
 <p>{calcado.preco}</p>
 </div>
 ))}
 </div>
 )}
 </div>
 )
}

export default ListaDeCalcados;

ListaDeCalcados > lista-de-calcados.css:

*{
 margin: 0;
 padding: 0;
 box-sizing: border-box;
}

.lista-de-calcados{
 width:100%;
 height: 100vh;
 background-color: #5f27cd;
}

.lista-de-calcados h2{
 width:100%;
 font-family: 'Arial Black', Courier, monospace;
 font-size: 28px;
 color: white;
 text-align: center;
 padding-top:40px;
}

.lista-de-calcados .calcados{
 display: flex;
 justify-content: center;
 align-items: center;
 margin-top:60px;
}

.lista-de-calcados .calcados .calcado{
 background-color: white;
 border-radius:12px;
 margin:15px;
 padding: 12px;
 text-align: center;
}

.lista-de-calcados .calcados .calcado p:nth-child(2){
 font-family: 'Arial Black', Courier, monospace;
 font-size: 16px;
 color: #1dd1a1;
 margin-bottom:8px;
}

Não se esqueça de adicionar o componente dentro do App.js:

import ListaDeCalcados from "./components/ListaDeCalcados";

export default function App() {
 return (
 <>
 <ListaDeCalcados />
 </>
 );
}

Além disso, segue o código do getCalcados.php:

<?php

header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST');
header("Access-Control-Allow-Headers: X-Requested-With");

// Simulando um retorno de uma API de Roupas

$array = array(
	array('id' => 225, 'nome' => 'Chinelo Nuvem Stripes V2', 'preco' => 'R$ 44,37', 'img' => 'http://localhost/api-roupas/nuvem-stripes-v2.png'),
	array('id' => 14, 'nome' => 'Mule Sw Shoes ', 'preco' => 'R$ 89,90', 'img' => 'http://localhost/api-roupas/mule-shoes.png'),
	array('id' => 922, 'nome' => 'Onça Mule', 'preco' => 'R$ 198,60', 'img' => 'http://localhost/api-roupas/onca-mule.png'),
	array('id' => 22, 'nome' => 'Sapato Show', 'preco' => 'R$ 88,00', 'img' => 'http://localhost/api-roupas/sapato-show.png'),
);

// Adicionando um atraso de 3 segundos
sleep(3);

// Simulando um erro de servidor
//http_response_code(500);
//echo "Erro interno do servidor";

echo json_encode($array);

Veja como ficou o resultado final:

Você deve ter percebido que nós ainda estamos chamando a nossa API pelo modelo antigo, mas fique tranquilo que iremos mudar essa realidade em breve.

Criando o Hook do TanStackQuery

De forma padrão, esta biblioteca nos intrui a criar uma nova pasta chamada de hooks dentro da pasta src:

Dentro dela, nós iremos criar os arquivos Javascript (chamados de hooks) que ficarão responsáveis por fazer chamadas ao nosso endpoint (getCalcados.php).

Para isso, dentro da pasta hooks vamos criar um novo arquivo chamado de useCalcadoData.js:

import { useQuery } from "@tanstack/react-query";
import axios from "axios";

const API_CALCADOS = '/getCalcados.php';

const fetchCalcados = async () => {
 const response = await axios.get(API_CALCADOS);
 return response.data;
}

export function useCalcadoData(){
 const query = useQuery({
 queryFn: fetchCalcados,
 queryKey: ['calcados-data']
 });

 return query;
}

Vamos explicar detalhadamente o que cada comando faz:

import { useQuery } from "@tanstack/react-query": este comando importa a função useQuery da biblioteca React Query, que será usada para buscar e gerenciar dados na sua aplicação.

import axios from "axios": inicialmente vamos fazer as nossas requisições utilizando a biblioteca do axios.

const API_CALCADOS: criamos uma constante que vai armazenar a URL da nossa API, e que será usada posteriormente pelo nosso arquivo. 

fetchCalcados: criamos uma função assíncrona que contém o código da requisição em axios que se comunica com nosso endpoint.

useCalcadoData: função que faz o uso da biblioteca React Query que será usada para executar a busca dos dados.

queryFn: função responsável por conectar a função que retorna os dados da requisição por meio do axios.

O queryKey nada mais é do que uma chave que identifica a requisição de forma única na nossa aplicação, de forma a dar um nome para os dados que estamos buscando.

Por exemplo, quando o nosso usuário executar um reload na página, inicialmente a aplicação puxaria novamente os dados da API, só que como o useQuery já tem um cache com esses dados salvos, a nossa aplicação fornece os dados que estão em cache, ou seja, enquanto os novos dados ainda estão esperando para serem retornados da API a biblioteca mostra os dados antigos.

E quando tais dados retornarem, a atualização vai acontecer na página.

Feito isso, vamos atualizar o código do nosso componente ListaDeCalcados:

import React from 'react';
import './lista-de-calcados.css';
import HttpService from '../../services/httpService';
import { useCalcadoData } from '../../hooks/useCalcado';

const ListaDeCalcados = () => {
 const { data } = useCalcadoData();//Toda vez que declaramos esse comando ele dispara a função e faz o fetch dos dados

 return (
 <div className="lista-de-calcados">
 <h2>📢 Calçados do Verão 🛒</h2>
 <div className="calcados">
 {data?.map((calcado, index) => (
 <div className="calcado" key={index}>
 <img src={calcado.img} alt={"Calçado " + (index + 1)} style={{ width: '180px', height: 'auto' }} />
 <p>{calcado.nome}</p>
 <p>{calcado.preco}</p>
 </div>
 ))}
 </div>
 </div>
 );
 
}

export default ListaDeCalcados;

Como podemos ver no código acima, tudo ficou bem mais simples.

Usamos o useCalcadoData para retornar os dados da requisição dentro da variável data.

Observação: Toda vez que declaramos o useCalcadoData o TanStackQuery dispara automaticamente a requisição do axios.

Observe que estamos utilizando o comando data?.map, com a presença do ponto de interrogação, que diz que o map so será executado quando o valor da variável data for preenchido, ou seja, só quando os dados forem retornados da nossa API.

Analisando a UI, podemos perceber que nada mudou:

Legal, agora vamos explorar um pouco mais os outros tipos de retorno dessa biblioteca 🙃

isLoading

A biblioteca também conta com o estado de isLoading que identifica se a requisição ainda está acontecendo ou se ela finalizou.

import React from 'react';
import './lista-de-calcados.css';
import HttpService from '../../services/httpService';
import { useCalcadoData } from '../../hooks/useCalcado';

const ListaDeCalcados = () => {
 const { data, isLoading } = useCalcadoData();//Toda vez que declaramos esse comando ele dispara a função e faz o fetch dos dados

 return (
 <div className="lista-de-calcados">
 <h2>📢 Calçados do Verão 🛒</h2>
 <div className="calcados">
 {isLoading && <p>Carregando...</p>}
 {!isLoading && <>
 {data?.map((calcado, index) => (
 <div className="calcado" key={index}>
 <img src={calcado.img} alt={"Calçado " + (index + 1)} style={{ width: '180px', height: 'auto' }} />
 <p>{calcado.nome}</p>
 <p>{calcado.preco}</p>
 </div>
 ))} </>
 } 
 </div>
 </div>
 );
 
}

export default ListaDeCalcados;

IsError

Também podemos tratar erros que podem acontecer durante a requisição com o estado isError, observe:

import React from 'react';
import './lista-de-calcados.css';
import HttpService from '../../services/httpService';
import { useCalcadoData } from '../../hooks/useCalcado';

const ListaDeCalcados = () => {
 const { data, isLoading, isError } = useCalcadoData();//Toda vez que declaramos esse comando ele dispara a função e faz o fetch dos dados

 return (
 <div className="lista-de-calcados">
 <h2>📢 Calçados do Verão 🛒</h2>
 <div className="calcados">
 {isLoading && <p>Carregando...</p>}
 {isError && <p>Ocorreu um erro ao carregar os dados</p>}
 {!isLoading && <>
 {data?.map((calcado, index) => (
 <div className="calcado" key={index}>
 <img src={calcado.img} alt={"Calçado " + (index + 1)} style={{ width: '180px', height: 'auto' }} />
 <p>{calcado.nome}</p>
 <p>{calcado.preco}</p>
 </div>
 ))} </>
 } 
 </div>
 </div>
 );
 
}

export default ListaDeCalcados;

O TanStackQuery costuma fazer um retry (nova tentativa de busca na API) antes de retornar um erro, ele faz isso para garantir que o possível erro ocorrido não seja algo momentâneo do servidor.

Caso desejar, você pode desativar essa opção de retry informando false na função do useQuery:

export function useCalcadoData(){
 const query = useQuery({
 queryFn: fetchCalcados,
 queryKey: ['calcados-data'],
 retry: false,
 });

 return query;
}

Fazendo com que se a primeira tentativa de requisição retornar um erro, o estado desse erro seja retornado sem a necessidade de uma segunda tentativa (sem o retry), ou seja:

Com o Retry: "Hey, parece que minha requisição retornou um errou, deixa eu só tentar mais uma vez... ok, o servidor está com problema mesmo, enfim, vou retornar esse erro."

Sem o Retry: "Hey, parece que minha requisição retornou um erro, enfim, vou retornar esse erro."

ReFetchInterval

Também podemos adicionar um intervalo em que a nossa aplicação deverá consumir a nossa API de tempos em tempos.

export function useCalcadoData(){
 const query = useQuery({
 queryFn: fetchCalcados,
 queryKey: ['calcados-data'],
 retry: false,
 refetchInterval: 60 * 7 * 1000
 });

 return query;
}

No código acima a cada 7 minutos a API irá fazer uma nova requisição de forma continua.

Enviando parâmetros para a biblioteca do TanStackQuery

Show! Até o momento nós aprendemos a fazer chamadas em APIs usando o método GET do axios em conjunto com a biblioteca TanStackQuery, mas talvez você esteja se perguntando:

"Como eu envio parâmetros via método GET para um determinado endpoint?".

Vamos pegar como exemplo o nosso componente ListaDeCalcados:

import React from 'react';
import './lista-de-calcados.css';
import { useCalcadoData } from '../../hooks/useCalcadoData';

const ListaDeCalcados = () => {
 const { data, isLoading, isError } = useCalcadoData();//Toda vez que declaramos esse comando ele dispara a função e faz o fetch dos dados

 return (
 <div className="lista-de-calcados">
 <h2>📢 Calçados do Verão 🛒</h2>
 <div className="calcados">
 {isLoading && <p>Carregando...</p>}
 {isError && <p>Ocorreu um erro ao carregar os dados</p>}
 {!isLoading && <>
 {data?.map((calcado, index) => (
 <div className="calcado" key={index}>
 <img src={calcado.img} alt={"Calçado " + (index + 1)} style={{ width: '180px', height: 'auto' }} />
 <p>{calcado.nome}</p>
 <p>{calcado.preco}</p>
 </div>
 ))} </>
 } 
 </div>
 </div>
 );
 
}

export default ListaDeCalcados;

Supondo que queremos enviar um ID específico diretamente para o hook useCalcadoData para que posteriormente ele envie tal parâmetro para axios, nós podemos fazer isso da seguinte forma:

ListaDeCalcados > index.jsx:

const ListaDeCalcados = () => {
 const calcadoId = 123; // ID do calcado específico
 const { data, isLoading, isError } = useCalcadoData(calcadoId);//Passamos o ID diretamente para o hook

 // Restante do código...
}

useCalcadoData.js:

import { useQuery } from "@tanstack/react-query";
import axios from "axios";

const API_CALCADOS = 'http://localhost/api-roupas/getCalcados.php';

const fetchCalcados = async (calcadoId) => {
 // Se um calcadoId for fornecido, busca apenas o calcado com esse ID
 if (calcadoId) {
 const response = await axios.get(`${API_CALCADOS}?id=${calcadoId}`);
 return response.data;
 }
 // Caso contrário, busca todos os calcados
 const response = await axios.get(API_CALCADOS);
 return response.data;
}

export function useCalcadoData(calcadoId){
 const query = useQuery({
 queryFn: () => fetchCalcados(calcadoId),
 queryKey: ['calcados-data', calcadoId], // Adiciona o calcadoId como parte da chave de consulta
 retry: false,
 refetchInterval: 60 * 7 * 1000
 });

 return query;
}

No comando acima estamos enviando um ID específico para a biblioteca do TanStackQuery que por sua vez faz o tratamento desse ID e o envia para o Axios.

Legal, mas vamos supor que o calcadoId ainda não esteja disponível para uso imediato, ou seja, o usuário tem que clicar em um botão para que a requisição aconteça.

Para isso você pode declarar normalmente a requisição e passar o estado calcadoId para dentro de useCalcadoData.js da seguinte forma:

ListaDeCalcados > index.jsx:

import React, { useState } from 'react';
import './lista-de-calcados.css';
import { useCalcadoData } from '../../hooks/useCalcadoData';

const ListaDeCalcados = () => {
 const [calcadoId, setCalcadoId] = useState(null); // Estado para armazenar o calcadoId
 const { data, isLoading, isError, refetch } = useCalcadoData(calcadoId); // Passamos o calcadoId para o hook

 // Função para buscar os dados do calcado com base no ID
 const handleBuscarCalcado = () => {
 // Aqui você pode definir o ID do calcado, por exemplo, após uma interação do usuário
 setCalcadoId(123); // Substitua 123 pelo ID desejado
 };

 return (
 <div className="lista-de-calcados">
 <h2>📢 Calçados do Verão 🛒</h2>
 <button onClick={handleBuscarCalcado}>Buscar Calcado</button> {/* Botão para buscar o calcado */}
 <div className="calcados">
 {isLoading && <p>Carregando...</p>}
 {isError && <p>Ocorreu um erro ao carregar os dados</p>}
 {!isLoading && <>
 {data?.map((calcado, index) => (
 <div className="calcado" key={index}>
 <img src={calcado.img} alt={"Calçado " + (index + 1)} style={{ width: '180px', height: 'auto' }} />
 <p>{calcado.nome}</p>
 <p>{calcado.preco}</p>
 </div>
 ))} </>
 } 
 </div>
 </div>
 );
}

export default ListaDeCalcados;

Quando clicamos no botão, automaticamente chamamos o setCalcadoId que por sua vez muda o estado de calcadoId, e como esse estado está conectado com useCalcadoData, o refetch acontece novamente.

Beleza, o único problema é que quando o useCalcadoData recebe o calacadoId, ele ainda está nulo:

useCalcadoData(calcadoId);//calcadoId ainda está null, pois o usuário ainda não clicou no botão

Então para evitar possíveis problemas de código, você pode adicionar mais um argumento dentro do useCalcadoData mais especificamente abaixo do queryFn chamado de enabled:

export function useCalcadoData(calcadoId){
 const query = useQuery({
 queryFn: fetchCalcados,
 queryKey: ['calcados-data'],
 enabled: !!calcadoId
 });

 return query;
}

Dessa forma, quando passamos o calcadoId para o enabled, a sua requisição só vai ser disparada quando o valor existente dentro de calcadoId for diferente de falso ou nulo.

Portanto, a requisição só iria ser disparada logo após o usuário clicar no botão e acionar a função handleBuscarCalcado, que por sua vez muda o estado do calcadoId por meio do setCalcadoId.

Usando o TanStackQuery em suas requisições do tipo POST

Agora, nós iremos aprender a utilizar a biblioteca em conjunto com as suas requisições do tipo POST.

O ponto bom em fazer requisições do tipo POST usando essa biblioteca, é que já podemos retornar os dados atualizados diretamente no front-end, sem a necessidade de ter que executar um outro fetch responsável por buscar tais dados.

Antes vamos criar um novo hook chamado useFormsMutate.js:

import { useMutation } from "@tanstack/react-query";

const postData = async (formData) => {
 return await fetch('http://localhost/api-roupas/submitForm.php', formData);
}

export function useFormsMutate(){
 const mutate = useMutation({
 mutationFn: postData,
 });

 return mutate;
}

Segundo a documentação do TanStackQuery, os hooks responsáveis por retornar dados devem ser nomeados como use + Nome do seu Hook + Data, por exemplo:

  • useCalcadoData.js
  • useRoupasData.js
  • useUsersData.js

Já os hooks responsáveis pelo envio de dados devems er nomeados como use + Nome do seu Hook + Mutate, por exemplo:

  • useFormsMutate.js
  • useLoginMutate.js
  • useSignupMutate.js

Em seguida vamos criar um novo componente chamado de FormsData.

FormsData > index.jsx:

import React, { useEffect, useState } from 'react';
import { useFormsMutate } from '../../hooks/useFormsMutate'; // Importando o hook personalizado

const FormData = () => {
 const [email, setEmail] = useState('');
 const [password, setPassword] = useState('');

 // Utilizando o hook personalizado useFormsMutate
 const { mutate, isSuccess, isError } = useFormsMutate();

 // Função para lidar com o envio do formulário
 const handleSubmit = async (event) => {
 event.preventDefault();

 const data = {
 email,
 password,
 }

 // Realizando a chamada de mutação para enviar os dados
 mutate(data);
 };

 useEffect(() => {
 if (isSuccess) {
 alert('Dados enviados com sucesso!');
 }
 if(isError){
 alert('Ocorreu um erro ao enviar os dados');
 }
 }, [isSuccess, isError]);

 return (
 <form onSubmit={handleSubmit}>
 <div>
 <label htmlFor="email">Email:</label>
 <input
 type="email"
 id="email"
 value={email}
 onChange={(e) => setEmail(e.target.value)}
 required
 />
 </div>
 <div>
 <label htmlFor="password">Senha:</label>
 <input
 type="password"
 id="password"
 value={password}
 onChange={(e) => setPassword(e.target.value)}
 required
 />
 </div>
 <button type="submit" disabled={mutate.isLoading}>
 {mutate.isLoading ? 'Enviando...' : 'Enviar'}
 </button>
 </form>
 );
};

export default FormData;

Note que estamos retornando as variáveis mutate, isSuccess e isError de dentro do useFormsMutate.

A variável mutate é usada para enviar uma chamada de mutação dos dados do formulário para o TanStackQuery, de modo que ele consiga fazer as requisições no nosso endpoint.

Temos também as variáveis isSuccess e isError que nos ajudam a identificar possíveis problemas que possam acontecer durante a nossa requisição.

Legal, atualmente o código acima faz uma requisição via método POST para uma determinada API, cumprindo o seu papel 🥳

Mas você se lembra que eu cheguei a comentar que também é possível realizar um fetch logo após o POST?

Veremos agora como isso funciona 😉

Realizando um fetch após uma requisição POST no TanStackQuery

Supondo que queremos que os dados que estão sendo mostrados na UI, fossem atualizados automaticamente logo após o envio da requisição POST, como fariamos isso?

Para isso, dentro do nosso hook useFormsMutate.js você vai precisar adicionar uma nova condicional chamada de onSuccess, e por fim chamar queryKey do useCalcadosDataque é responsável por trazer os dados dos calçados:

import { useMutation, useQueryClient } from "@tanstack/react-query";

const postData = async (formData) => {
 return await fetch('http://localhost/api-roupas/submitForm.php', formData);
}

export function useFormsMutate(){
 const queryClient = useQueryClient();//Chama a instância do cliente de query que foi definida no App.js
 const mutate = useMutation({
 mutationFn: postData,
 onSuccess: () => {
 queryClient.invalidateQueries(['calcados-data']);
 }
 });

 return mutate;
}

A função invalidateQueries vai invalidar a query que definimos no hook useFormsData.js. Neste caso específico, está invalidando uma query chamada 'calcados-data'.

Quando você executa uma query usando o React Query, ele armazena o resultado dessa query em cache, e isso significa que se você executar a mesma query novamente, ele pode retornar os dados diretamente do cache em vez de fazer uma nova solicitação ao servidor.

No entanto, às vezes você precisa forçar o React Query a buscar novamente os dados do servidor, mesmo que eles já estejam em cache.

É aí que invalidateQueries entra em cena, pois essa função é usada justamente para marcar uma ou mais queries como "inválidas", o que significa que o React Query precisará buscar novamente os dados da próxima vez que a query for executada.

Isso só funciona, pois de baixo dos panos, todos esses hooks que fazem o uso do React Query estão interconectados e se comunicam uns com os outros, e isso faz sentido, pois se você parar para pensar, o React Query funciona como se fosse uma espécie de contexto global.

Usando o módulo HTTP em conjunto com o React Query

Para usar o módulo HTTP que criamos na lição passada, você pode fazer isso de uma maneira bem simples, observe:

useCalcadosData()

import { useQuery } from "@tanstack/react-query";
import HttpService from "../services/httpService";

const API_CALCADOS = '/getCalcados.php'; // Remova 'http://localhost/api-roupas'

const httpService = new HttpService(); // Crie uma instância do HttpService

const fetchCalcados = async () => {
 try {
 return await httpService.get(API_CALCADOS); // Use httpService.get ao invés de axios.get
 } catch (error) {
 throw error; // Rejeita a promessa com o erro
 }
}

export function useCalcadoData(){
 const query = useQuery({
 queryFn: fetchCalcados,
 queryKey: ['calcados-data'],
 retry: false,
 refetchInterval: 60 * 7 * 1000
 });

 return query;
}

Caso desejar você também pode só retornar a resposta do módulo em vez de encapsular toda a lógica dentro de bloco do tipo try...catch:

const fetchCalcados = () => {
 return httpService.get(API_CALCADOS); // Use httpService.get ao invés de axios.get
}

Arquivos da lição

Os arquivos dessa lição podem ser encontrados neste repositório do GitHub.

Conclusão

Nesta lição você aprendeu a utilizar a biblioteca TanStackQuery (antiga React Query).

E acredito que a partir de agora você dará preferência a essa biblioteca não é verdade?

Sim! Eu a utilizo na maioria dos meus projetos em conjunto com o Axios e o módulo HttpService 😁

Até a próxima lição!