Padrões de busca de dados no React

Introdução

Padrões de data fetching são estratégias que podem ser utilizadas para melhorar a performance percebida da aplicação. Ao utilizar esses padrões, proporcionamos aos usuários a sensação de que a aplicação é mais rápida do que realmente é. Existem várias maneiras de alcançar isso em uma aplicação client-side, cada uma igualmente importante e servindo seu propósito único de melhorar a Experiência do Usuário e até mesmo a Experiência do Desenvolvedor.

Formas de caching

kylo

Cache em memória é um dos mecanismos mais importantes em aplicações client-side que pode ser usado para melhorar a performance percebida pelos usuários. No entanto, é importante mencionar que o cache em memória é diferente do cache do navegador. Enquanto o cache do navegador é algo habilitado por padrão nos navegadores modernos, o cache em memória é controlado pela própria aplicação e pode ser feito manualmente ou usando bibliotecas de terceiros, além de poder ser persistido usando as opções nativas de armazenamento disponíveis nos navegadores.

Cache em memória é uma estratégia que consiste em salvar os dados buscados em memória, geralmente usando um Map para maior eficiência. Uma maneira simples de implementar isso seria:

1	 // Implementão simples de cache em memória
2	 const cache = new Map()
3	 
4	 // Busca no cache
5	 const getCachedData = (key) => {
6	   const item = cache.get(key)
7	   
8	   if (!item) return null
9	   
10	   const now = Date.now()
11	   
12	   if (now > item.expiry) {
13	     cache.delete(key)
14	     
15	     return null
16	   }
17	   
18	   return item.data
19	 }
20	 
21	 // Grava no cache
22	 const setCacheData = (key, data, ttlSeconds = 300) => {
23	   cache.set(key, {
24	     data,
25	     expiry: Date.now() + (ttlSeconds * 1000)
26	   })
27	 }
28	 
29	 // Uso
30	 const loadData = async (id) => {
31	   const cachedData = getCachedData(id)
32	 
33	   if (cachedData) return cachedData
34	 
35	   const { data } = await myService.getById(id)
36	   setCacheData(id, data)
37	 
38	   return data
39	 }
40	 

Embora esta implementação possa parecer suficiente, existem situações que precisam ser consideradas como: tratamento de erros, robustez, performance, condições de corrida, dados desatualizados, gerenciamento de estado e várias outras questões que podem surgir. Portanto, uma abordagem mais conveniente seria usar algo que já está sendo utilizado e testado por milhões de outros desenvolvedores, como o Tanstack Query.

Tanstack Query

Basicamente, Tanstack Query é um wrapper em torno de requisições HTTP que gerencia queries e mutations em aplicações client-side. Esta ferramenta permite que os desenvolvedores se concentrem em construir ótimas experiências enquanto deixam a complexidade de lidar com o cache para a própria ferramenta.

Embora o Tanstack Query gerencie o cache por conta própria, ele também fornece um controle refinado sobre o cache armazenado em memória. Além disso, possui uma série de métodos integrados que permitem que a aplicação intervenha e assuma o controle do que está acontecendo nos bastidores.

A configuração básica de uma instância Tanstack Query para uma aplicação React seria:

1	 import {
2	   useQuery,
3	   useMutation,
4	   useQueryClient,
5	   QueryClient,
6	   QueryClientProvider,
7	 } from '@tanstack/react-query'
8	 import { getTodos, postTodo } from '../my-api'
9	 
10	 // Cria a instância
11	 const queryClient = new QueryClient()
12	 
13	 function App() {
14	   return (
15	     // Fornece a instância pro contexto
16	     <QueryClientProvider client={queryClient}>
17	       <Todos />
18	     </QueryClientProvider>
19	   )
20	 }
21	 
22	 function Todos() {
23	   // Acessa instância 
24	   const queryClient = useQueryClient()
25	 
26	   // Busca os dados
27	   const query = useQuery({ queryKey: ['todos'], queryFn: getTodos })
28	 
29	   // Muda os dados
30	   const mutation = useMutation({
31	     mutationFn: postTodo,
32	     onSuccess: () => {
33	       // Invalida e busca novamente
34	       queryClient.invalidateQueries({ queryKey: ['todos'] })
35	     },
36	   })
37	 
38	   return (
39	     <div>
40	       <ul>{query.data?.map((todo) => <li key={todo.id}>{todo.title}</li>)}</ul>
41	 
42	       <button
43	         onClick={() => {
44	           mutation.mutate({
45	             id: Date.now(),
46	             title: 'Do Laundry',
47	           })
48	         }}
49	       >
50	         Add Todo
51	       </button>
52	     </div>
53	   )
54	 }
55	 
56	 render(<App />, document.getElementById('root'))
57	 
58	 // Fonte: https://tanstack.com/query/latest/docs/framework/react/quick-start
59	 

O exemplo acima ilustra o básico de como trabalhar com Tanstack Query. Ele cobre a criação de um query client estável, envolvendo sua aplicação com um QueryClientProvider, acessando o query client de dentro de um componente em qualquer lugar na árvore do React, fazendo uma query, realizando uma mutation e invalidando os dados desatualizados de uma query específica.

Como funciona o cache do Tanstack Query?

O cache do Tanstack Query funciona em torno de um conceito chamado query key. Cada query deve ter uma query key associada, que é simplesmente um array de valores usado para distinguir uma query de outra. A query key é usada como chave para aquela query específica dentro da estrutura de dados do cache em memória. Com essa chave, é fácil para a aplicação manipular os dados de uma query específica.

1	 import { useQuery } from '@tanstack/react-query'
2	 import { getUser } from '../my-api'
3	 
4	 function User() {
5	   // Query key usada para cachear os dados
6	   const queryKey = ['user', '1']
7	   
8	   const query = useQuery({ queryKey, queryFn: getTodos })
9	 
10	   return (
11	     <div>
12	 	  <span>{query.data?.name}</span>
13	 	  <span>{query.data?.address}</span>
14	     </div>
15	   )
16	 }
17	 

Agora, os dados para aquele usuário específico estão em cache, e sempre que a aplicação precisar desses dados, ela usará sua versão em cache e revalidará em segundo plano, o que permite que os usuários tenham uma melhor experiência, pois não precisam esperar por dados que já foram buscados anteriormente. Além disso, agora os dados em cache podem ser acessados e manipulados usando:

Também é possível invalidar queries baseado em suas query keys:

Outro aspecto importante de caching são os dados desatualizados (stale data). Os dados são considerados desatualizados quando a aplicação não possui mais sua versão mais atualizada. O Tanstack Query tem uma opção de stale time, que é importante para gerenciar por quanto tempo os dados buscados pelas queries são considerados fresh (novos). O stale time tem valor padrão de 0 milissegundos, o que significa que as queries serão atualizadas em segundo plano toda vez que o componente for montado ou quando a janela for refocada. O stale time pode ser personalizado de acordo com as necessidades da aplicação para otimizar a performance e reduzir buscas desnecessárias de dados.

1	 const { data } = useQuery({ 
2	   queryKey: ['todos'], 
3	   queryFn: fetchTodos, 
4	   staleTime: 60 * 1000, // 60 segundos 
5	 });
6	 

No exemplo acima, toda vez que uma query com uma query key definida como ['todos'] for feita dentro de 60 segundos, o Tanstack Query usará os dados armazenados dentro do cache em vez de chamar a API novamente.

Alternativamente, um stale time padrão pode ser definido em um query client e compartilhado com todas as queries, a menos que seja sobrescrito pela própria query.

1	 const queryClient = new QueryClient({ 
2	   defaultOptions: { 
3	     queries: { 
4	       staleTime: 60 * 1000, // 60 segundos 
5	     }, 
6	   }, 
7	 });
8	 

Para mais informações sobre caching no Tanstack Query:

Dados iniciais vs dados placeholder

As opções de dados iniciais e dados placeholder são duas maneiras diferentes de informar como a query precisa se comportar quando não há dados armazenados em cache.

A opção de dados iniciais fornece à query os dados (não parciais) que serão exibidos inicialmente na aplicação, pulando o estado de carregamento e cacheando seu valor, sendo útil quando os dados já estão disponíveis de alguma forma. No entanto, como o stale time tem valor padrão de 0 milissegundos, a query será revalidada imediatamente, então para evitar isso o seguinte pode ser feito:

1	 // Mostra initialTodos imediatamente, mas não irá buscar novamente os dados até que outro evento de interação seja encontrado após 1000 ms
2	 const result = useQuery({
3	   queryKey: ['todos'],
4	   queryFn: () => fetch('/todos'),
5	   initialData: initialTodos,
6	   staleTime: 60 * 1000, // 1 minuto
7	   // Isso poderia ser 10 segundos atrás ou 10 minutos atrás
8	   initialDataUpdatedAt: initialTodosUpdatedTimestamp, // ex. 1608412420052
9	 })
10	 
11	 // Fonte: https://tanstack.com/query/latest/docs/framework/react/guides/initial-query-data
12	 

Ao comparar a diferença entre staleTime e initialDataUpdatedAt, a query sabe se os dados iniciais estão novos ou não e precisam ser revalidados.

Outra opção é passar uma função como dados iniciais para a query. A função será executada apenas uma vez quando a query for inicializada.

1	 const result = useQuery({
2	   queryKey: ['todo', todoId],
3	   queryFn: () => fetch(`/todos/${todoId}`),
4	   staleTime: 60 * 1000, // 1 minuto
5	   initialData: () => queryClient.getQueryData(['todos'])?.find((d) => d.id === todoId), // Usa um todo da query 'todos' como dados iniciais para esta query
6	   initialDataUpdatedAt: () => queryClient.getQueryState(['todos'])?.dataUpdatedAt // Obtém a última atualização da query 'todos'
7	 })
8	 

Mais uma coisa importante ilustrada no exemplo acima é como o cache de outras queries pode ser usado para popular o cache de outra query com a opção de dados iniciais. Usando o query client e seus métodos integrados getQueryData e getQueryState, é possível acessar os dados e o estado de outras queries e usá-los conforme a aplicação necessitar.

Por outro lado, há a opção de dados placeholder que permite que as queries se comportem como se já tivessem dados, similar à opção de dados iniciais, mas não persiste os dados no cache. É útil quando há dados parciais ou falsos suficientes que podem ser exibidos antecipadamente enquanto os dados reais são buscados em segundo plano.

1	 function Todos() {
2	   const result = useQuery({
3	     queryKey: ['todos'],
4	     queryFn: () => fetch('/todos'),
5	     placeholderData: placeholderTodos,
6	   })
7	 }
8	 
9	 // Fonte: https://tanstack.com/query/latest/docs/framework/react/guides/placeholder-query-data
10	 

Adicionalmente, a versão de função da opção de dados placeholder tem acesso aos dados anteriores daquela query, o que significa que é possível evitar mostrar estados de carregamento ao realizar buscas com queries dinâmicas como: buscar algo por seu id, ou uma query paginada.

1	 const result = useQuery({
2	   queryKey: ['todos', id],
3	   queryFn: () => fetch(`/todos/${id}`),
4	   placeholderData: (previousData, previousQuery) => previousData,
5	 })
6	 
7	 // Fonte: https://tanstack.com/query/latest/docs/framework/react/guides/placeholder-query-data
8	 

ou simplesmente:

1	 import { useQuery, keepPreviousData } from '@tanstack/react-query'
2	 
3	 const result = useQuery({
4	   queryKey: ['todos', id],
5	   queryFn: () => fetch(`/todos/${id}`),
6	   placeholderData: keepPreviousData
7	 })
8	 

Prefetching de dados

Ao renderizar componentes, frequentemente existem casos onde um componente filho que precisa buscar algum dado necessita que a query do pai termine de carregar para que possa ser renderizado e então começar a buscar os dados que realmente precisa. Isso gera o que é chamado de request waterfall e pode levar a uma experiência ruim do usuário e baixa performance. Por exemplo:

1	 function Article({ id }) {
2	   const { data: articleData, isPending } = useQuery({
3	     queryKey: ['article', id],
4	     queryFn: getArticleById,
5	   })
6	 
7	   if (isPending) {
8	     return 'Loading article...'
9	   }
10	 
11	   return (
12	     <>
13	       <ArticleHeader articleData={articleData} />
14	       <ArticleBody articleData={articleData} />
15	       <Comments id={id} />
16	     </>
17	   )
18	 }
19	 
20	 function Comments({ id }) {
21	   const { data, isPending } = useQuery({
22	     queryKey: ['article-comments', id],
23	     queryFn: getArticleCommentsById,
24	   })
25	 
26	   ...
27	 }
28	   
29	 // Fonte: https://tanstack.com/query/latest/docs/framework/react/guides/prefetching
30	 

Isso gera a seguinte cascata:

1	 1. |> getArticleById()
2	 2.   |> getArticleCommentsById()
3	 
4	 Fonte: https://tanstack.com/query/latest/docs/framework/react/guides/prefetching
5	 

Uma abordagem para contornar isso é fazer o prefetch dos dados assim:

1	 function Article({ id }) {
2	   const { data: articleData, isPending } = useQuery({
3	     queryKey: ['article', id],
4	     queryFn: getArticleById,
5	   })
6	 
7	   // Prefetch
8	   useQuery({
9	     queryKey: ['article-comments', id],
10	     queryFn: getArticleCommentsById,
11	     // Otimização opcional para evitar re-renderizações quando esta query muda:
12	     notifyOnChangeProps: [],
13	   })
14	 
15	   if (isPending) {
16	     return 'Loading article...'
17	   }
18	 
19	   return (
20	     <>
21	       <ArticleHeader articleData={articleData} />
22	       <ArticleBody articleData={articleData} />
23	       <Comments id={id} />
24	     </>
25	   )
26	 }
27	 
28	 function Comments({ id }) {
29	   const { data, isPending } = useQuery({
30	     queryKey: ['article-comments', id],
31	     queryFn: getArticleCommentsById,
32	   })
33	 
34	   ...
35	 }
36	 
37	 // Fonte: https://tanstack.com/query/latest/docs/framework/react/guides/prefetching
38	 

Agora a cascata se parece com:

1	 1. |> getArticleById()
2	 1. |> getArticleCommentsById()
3	 
4	 Fonte: https://tanstack.com/query/latest/docs/framework/react/guides/prefetching
5	 

Além disso, é possível fazer prefetch de dados dentro da função queryFn de uma query e para isso é possível usar o query client:

1	 const queryClient = useQueryClient()
2	 
3	 const { data: articleData, isPending } = useQuery({
4	   queryKey: ['article', id],
5	   queryFn: (...args) => {
6	     queryClient.prefetchQuery({
7	       queryKey: ['article-comments', id],
8	       queryFn: getArticleCommentsById,
9	     })
10	 
11	     return getArticleById(...args)
12	   },
13	 })
14	 
15	 // Fonte: https://tanstack.com/query/latest/docs/framework/react/guides/prefetching
16	 

Note que se o componente estiver sendo suspenso, a aplicação precisa usar usePrefetchQuery e useSuspenseQuery. Para mais informações sobre isso:

Outra forma de fazer prefetch de dados é identificando as intenções do usuário com event handlers e então buscando os dados que possivelmente serão necessários. É possível fazer isso usando o método prefetchQuery.

Este método apenas popula o cache da query e não retorna nada.

1	 function ShowDetailsButton() {
2	   const queryClient = useQueryClient()
3	 
4	   const prefetch = () => {
5	     queryClient.prefetchQuery({
6	       queryKey: ['details'],
7	       queryFn: getDetailsData,
8	       // Prefetch só é disparado quando os dados são mais antigos que o staleTime,
9	       // então em um caso como este você definitivamente quer definir um
10	       staleTime: 60000,
11	     })
12	   }
13	 
14	   return (
15	     <button onMouseEnter={prefetch} onFocus={prefetch} onClick={...}>
16	       Show Details
17	     </button>
18	   )
19	 }
20	 
21	 // Fonte: https://tanstack.com/query/latest/docs/framework/react/guides/prefetching
22	 

Fazendo isso, toda vez que o usuário passar o mouse ou focar no elemento, os dados serão pré-buscados. É importante definir um stale time nesses casos para que a aplicação não faça muitas requisições desnecessárias ao servidor.

Garantindo os dados da query

O Tanstack Query fornece um método para popular o cache do query client chamado ensureQueryData que pode ser usado para garantir que os dados de uma determinada query estejam disponíveis quando necessário. Garantir que os dados da query estejam lá significa que o método vai verificar se a query que está sendo feita tem dados em cache que não estão obsoletos e retorná-los, e se a query ainda não tiver dados em cache, o método ensureQueryData chama a API internamente, salva sua resposta no cache e retorna os dados.

1	 const data = await queryClient.ensureQueryData({ queryKey, queryFn })
2	 
3	 // Fonte: https://tanstack.com/query/latest/docs/reference/QueryClient#queryclientensurequerydata
4	 

Isso é útil quando a aplicação precisa fazer prefetch de dados que podem já estar em cache. Por exemplo, dentro de loaders de rotas, pois a página poderia ser acessada diretamente via URL e isso exigiria que todos os dados fossem carregados do zero dentro do loader, ou a página poderia ser aberta pelo usuário navegando até ela usando um link que pode ser usado para fazer prefetch dos dados da página de destino quando o mouse estiver sobre ele, o que significa que o loader da rota não precisa pré-buscar os dados novamente.

Queries infinitas

Também é possível realizar queries baseadas em cursor com carregamento infinito usando o Tanstack Query. Usando os hooks useInfiniteQuery, useSuspenseInfiniteQuery e usePrefetchInfiniteQuery, é possível carregar infinitamente qualquer query baseada em cursor em qualquer situação que a aplicação possa enfrentar.

1	 import { useInfiniteQuery } from '@tanstack/react-query'
2	 
3	 function Projects() {
4	   const fetchProjects = async ({ pageParam }) => {
5	     const res = await fetch('/api/projects?cursor=' + pageParam)
6	     return res.json()
7	   }
8	 
9	   const {
10	     data,
11	     error,
12	     fetchNextPage,
13	     hasNextPage,
14	     isFetching,
15	     isFetchingNextPage,
16	     status,
17	   } = useInfiniteQuery({
18	     queryKey: ['projects'],
19	     queryFn: fetchProjects,
20	     initialPageParam: 0,
21	     getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
22	   })
23	 
24	   return status === 'pending' ? (
25	     <p>Loading...</p>
26	   ) : status === 'error' ? (
27	     <p>Error: {error.message}</p>
28	   ) : (
29	     <>
30	       {data.pages.map((group, i) => (
31	         <React.Fragment key={i}>
32	           {group.data.map((project) => (
33	             <p key={project.id}>{project.name}</p>
34	           ))}
35	         </React.Fragment>
36	       ))}
37	       <div>
38	         <button
39	           onClick={() => fetchNextPage()}
40	           disabled={!hasNextPage || isFetchingNextPage}
41	         >
42	           {isFetchingNextPage
43	             ? 'Loading more...'
44	             : hasNextPage
45	               ? 'Load More'
46	               : 'Nothing more to load'}
47	         </button>
48	       </div>
49	       <div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div>
50	     </>
51	   )
52	 }
53	 
54	 // Fonte: https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries
55	 

Além disso, é possível fazer prefetch dos dados de queries infinitas dentro de effects, callbacks e fora do React usando:

Para mais detalhes sobre as opções de query infinita:

Opções de query infinita

Devtools

O Tanstack Query vem com um componente integrado que ajuda os desenvolvedores a debugar as queries e mutations sendo feitas pela aplicação. Com este componente, é possível ver o estado das queries e mutations, bem como manipular dados e disparar eventos manualmente. É altamente recomendado ter isso instalado na aplicação.

O estado de uma query pode ser:

  • Fresh (Novo)
  • Fetching (Buscando)
  • Paused (Pausado)
  • Stale (Obsoleto)
  • Inactive (Inativo)

Devtools

Loaders de rotas

A maioria dos roteadores client-side modernos permite que as aplicações tenham uma função loader que executa fora do framework sendo usado, o que significa que o loader será executado antes do framework renderizar a página.

No React Router Dom, desde a versão 6, é possível configurar um loader para cada rota individual da seguinte forma:

1	 createBrowserRouter([
2	   {
3	     path: "/teams/:teamId",
4	     loader: ({ params }) => {
5	       return fakeGetTeam(params.teamId)
6	     }
7	   }
8	 ])
9	 
10	 // Fonte: https://reactrouter.com/en/main/route/loader
11	 

Usando loaders, é possível recuperar dados de qualquer lugar, como serviços externos, e disponibilizá-los para o componente sendo renderizado nesta rota.

1	 function loader({ request }) {
2	   const url = new URL(request.url)
3	   const searchTerm = url.searchParams.get("q")
4	   return searchProducts(searchTerm)
5	 }
6	 
7	 // Fonte: https://reactrouter.com/en/main/route/loader
8	 

Então é possível acessar os dados usando o hook useLoaderData:

1	 function SomeRoute() {
2	   const data = useLoaderData()
3	   // { some: "thing" }
4	 }
5	 
6	 // Fonte: https://reactrouter.com/en/main/route/loader
7	 

Outra coisa importante a se notar é que ao fazer lazy loading de componentes, o React Router Dom vai chamar as funções lazy e loader ao mesmo tempo, então quando o navegador terminar de baixar o javascript para renderizar a página, é altamente possível que o loader tenha terminado de executar, o que significa que os dados estarão disponíveis quase instantaneamente.

Integrando loaders de rotas e Tanstack Query

Embora seja possível usar apenas as capacidades do roteador para disponibilizar dados antecipadamente na aplicação, é ainda melhor combinar ambos e aproveitar seus pontos fortes.

Dito isso, a aplicação pode aproveitar a forma como os loaders funcionam e o momento em que são executados no ciclo de vida para fazer prefetch de recursos usando Tanstack Query.

1	 import { createBrowserRouter } from 'react-router-dom'
2	 import { QueryClient } from '@tanstack/react-query'
3	 import { getUsers } from '@api/get-users'
4	 
5	 const queryClient = new QueryClient()
6	 
7	 createBrowserRouter([
8	   {
9	     path: "/some-route",
10	     loader: async () => {
11	       // Garante que toda query que tem ['users'] como query key tenha dados em cache
12	       await queryClient.ensureQueryData({
13	         queryKey: ['users'],
14	         queryFn: getUsers
15	       })
16	 
17	       // Precisa retornar algo, mesmo que seja null
18	       return null
19	     }
20	   }
21	 ])
22	 

Boas práticas e performance

Existem algumas boas práticas que são aconselhadas a serem seguidas ao usar o Tanstack Query. Ao garantir que essas práticas são seguidas, é mais provável que a performance percebida, manutenibilidade e UX da aplicação sejam mantidas em um bom nível.

Aguardando promises

Em relação à performance de prefetch de dados de queries, é importante prestar atenção ao código bloqueante, pois às vezes aguardar uma promise ser resolvida não é necessário para a aplicação.

1	 // Código não bloqueante
2	 function prefetch() {
3	   queryClient.prefetchQuery(queryOptions)
4	 }
5	 
6	 // Código bloqueante
7	 async function prefetch() {
8	   await queryClient.prefetchQuery(queryOptions)
9	 }
10	 

Não há nada depois da chamada prefetchQuery, então não é necessariamente preciso aguardar a promise, a menos que seja intencional.

Outra coisa importante a se ter em mente é usar concorrência de promises quando necessário. Às vezes uma função precisa fazer prefetch de mais de uma query ao mesmo tempo e isso pode levar a uma performance ruim e tempos de carregamento longos se as promises não forem resolvidas concorrentemente.

1	 // Ruim - aguarda cada promise ser resolvida
2	 function prefetch() {
3	   await queryClient.prefetchQuery(queryOptions1)
4	   await queryClient.prefetchQuery(queryOptions2)
5	   await queryClient.prefetchQuery(queryOptions3)
6	 }
7	 
8	 // Bom - resolve promises concorrentemente
9	 async function prefetch() {
10	   await Promise.all([
11	     queryClient.prefetchQuery(queryOptions1),
12	     queryClient.prefetchQuery(queryOptions2),
13	     queryClient.prefetchQuery(queryOptions3),
14	   ])
15	 }
16	 

Note que às vezes uma query pode precisar de uma informação que retorna de outra query, então fazer prefetch da query antecipadamente ou persistir os valores importantes em lugares que podem ser acessados globalmente como o local storage ou a URL podem ser boas estratégias.

Reusabilidade e isolamento

Uma boa prática adicional é isolar queries em funções para que possam ser reutilizadas facilmente em diferentes lugares. Além disso, é importante lembrar de expor as coisas importantes e ao mesmo tempo manter um bom nível de encapsulamento. O Tanstack Query tem uma função auxiliar chamada queryOptions que facilita isso:

1	 // users-query-options.ts
2	 import { getUsers } from "@/services/users-service/users-service"
3	 import { keepPreviousData, queryOptions } from "@tanstack/react-query"
4	 
5	 export const usersQueryOptions = queryOptions({
6	   queryKey: ["users"],
7	   queryFn: getUsers,
8	   placeholderData: keepPreviousData, // Evita transições bruscas de dados
9	 })
10	 
11	 // query.types.ts
12	 export type QueryParams<TData, TReturn> = {
13	   select?: (data: TData) => TReturn // Habilita mapeamento de dados da query
14	   initialData?: TData | (() => TData)
15	 }
16	 
17	 // use-users.ts
18	 import { usersQueryOptions } from "@/lib/react-query/users-query-options/users-query-options"
19	 import { useQuery } from "@tanstack/react-query"
20	 
21	 import type { QueryParams } from "@/lib/react-query/query.types"
22	 import type { ApiUser } from "@/services/users-service/users-service"
23	 
24	 export function useUsers<TReturn = ApiUser[]>({
25	   select,
26	   initialData,
27	 }: QueryParams<ApiUser[], TReturn> = {}) {
28	   const { data, isLoading, isFetching, isPending, isError, error, refetch } = useQuery({
29	     ...usersQueryOptions,
30	     select,
31	     initialData,
32	   })
33	 
34	   return {
35	     data,
36	     isLoading,
37	     isFetching,
38	     isPending,
39	     isError,
40	     error,
41	     refetch,
42	   }
43	 }
44	 

O hook React personalizado acima é uma boa estratégia para isolar queries. Ele expõe os métodos select e initialData e retorna apenas um subconjunto do objeto retornado pelo useQuery. Observe como o hook é tipado usando generics, permitindo que o tipo de dados retornado seja inferido a partir do retorno do método select que tem como padrão o tipo de retorno original, isso é extremamente importante para a segurança de tipos.

Uso do hook personalizado:

1	 // users.tsx
2	 import { useUsers } from "@/hooks/use-users/use-users"
3	 import { UserMapper } from "@/mappers/user-mapper/user-mapper"
4	 
5	 const userMapper = UserMapper.create() // conjunto estável de métodos
6	 
7	 export function Users() {
8	   const { data: users = [] } = useUsers() // data é inferido como um array de api users
9	   const { data: mappedUsers = [] } = useUsers({ select: userMapper.mapMany }) // data é inferido como um array de mapped users;
10	   const { data: usersCount = 0 } = useUsers({ select: userMapper.count }) // data é inferido como um número;
11	 
12	   return ...
13	 }
14	 

Adicionalmente, é importante usar funções estáveis nos parâmetros select e initialData pois isso otimiza o processo de renderização. Funções estáveis podem ser funções criadas ou importadas de fora do contexto do React (hooks, componentes, etc...), ou funções que foram memoizadas pelo useCallback. Para mais informações veja:

Otimizações de renderização

Query client

Além das coisas mencionadas acima, é importante definir valores padrão para opções importantes na instância do query client.

1	 import { QueryClient } from "@tanstack/react-query";
2	 
3	 const queryClient = new QueryClient({
4	   defaultOptions: {
5	     queries: {
6	       retry: false, // Defina isto como true apenas em queries específicas se necessário
7	       refetchOnWindowFocus: false,
8	       staleTime: 1000 * 60, // 1 minuto para cada query, a menos que seja sobrescrito pela própria query
9	     },
10	   }
11	 });
12	 

Linting

O Tanstack Query fornece uma ferramenta de linting para garantir as melhores práticas ao usá-lo. Para mais detalhes sobre como configurá-la, veja:

Plugin ESLint query