Fumadocs + Code Hike
Frontend

TanStack Query

Biblioteca poderosa para gerenciamento de estado de servidor em React

O que é TanStack Query?

TanStack Query (anteriormente React Query) é uma biblioteca para fetching, caching, sincronização e atualização de estado de servidor em aplicações React. Simplifica drasticamente o gerenciamento de dados assíncronos, cache e sincronização com o backend.

Por que utilizamos TanStack Query na IngenioLab?

  • Cache inteligente: Sistema de cache automático com invalidação
  • Background refetching: Atualização de dados em background
  • Optimistic updates: Updates otimistas para melhor UX
  • DevTools: Ferramentas de debug excelentes
  • TypeScript first: Suporte completo a tipos
  • Menos boilerplate: Reduz código repetitivo para API calls

Instalação

# TanStack Query v5
npm install @tanstack/react-query
# DevTools (opcional, recomendado para desenvolvimento)
npm install @tanstack/react-query-devtools

Configuração Base IngenioLab

1. Setup do Query Client:

// src/lib/queryClient.ts
import { QueryClient } from '@tanstack/react-query'
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
// Tempo de cache (5 minutos)
staleTime: 1000 * 60 * 5,
// Tempo para garbage collection (10 minutos)
gcTime: 1000 * 60 * 10,
// Retry automático em caso de falha
retry: 3,
// Refetch quando a aba ganha foco
refetchOnWindowFocus: false,
// Refetch quando reconecta
refetchOnReconnect: true,
},
mutations: {
// Retry para mutations
retry: 1,
}
}
})

2. Provider da aplicação:

// src/App.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { queryClient } from './lib/queryClient'
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<div className="App">
{/* Sua aplicação */}
<Routes />
</div>
{/* DevTools apenas em desenvolvimento */}
{process.env.NODE_ENV === 'development' && (
<ReactQueryDevtools initialIsOpen={false} />
)}
</QueryClientProvider>
)
}

Hooks Principais

1. useQuery - Fetching de dados:

// src/hooks/useUsers.ts
import { useQuery } from '@tanstack/react-query'
import { User } from '../types/user'
import { apiClient } from '../services/api'
// Service para API
const userService = {
getAll: async (): Promise<User[]> => {
const response = await apiClient.get('/users')
return response.data
},
getById: async (id: string): Promise<User> => {
const response = await apiClient.get(`/users/${id}`)
return response.data
}
}
// Hook para listar usuários
export const useUsers = () => {
return useQuery({
queryKey: ['users'], // Chave única para cache
queryFn: userService.getAll,
staleTime: 1000 * 60 * 5, // 5 minutos
})
}
// Hook para usuário específico
export const useUser = (userId: string) => {
return useQuery({
queryKey: ['users', userId],
queryFn: () => userService.getById(userId),
enabled: !!userId, // Só executa se userId existir
})
}
// Hook com parâmetros de filtro
export const useUsersWithFilter = (filters: {
search?: string
role?: string
isActive?: boolean
}) => {
return useQuery({
queryKey: ['users', filters],
queryFn: () => userService.getFiltered(filters),
// Reexecutar quando filtros mudarem
enabled: Object.values(filters).some(Boolean),
})
}

2. Usando useQuery nos componentes:

// src/components/UserList.tsx
import { FC } from 'react'
import { useUsers } from '../hooks/useUsers'
import { LoadingSpinner } from './ui/LoadingSpinner'
import { ErrorMessage } from './ui/ErrorMessage'
export const UserList: FC = () => {
const {
data: users,
isLoading,
isError,
error,
refetch,
isFetching // Para loading states durante refetch
} = useUsers()
if (isLoading) {
return <LoadingSpinner />
}
if (isError) {
return (
<ErrorMessage
message={error?.message || 'Erro ao carregar usuários'}
onRetry={refetch}
/>
)
}
return (
<div className="user-list">
<div className="header">
<h2>Usuários {isFetching && '(Atualizando...)'}</h2>
<button onClick={() => refetch()}>
Recarregar
</button>
</div>
{users?.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
)
}

3. useMutation - Modificando dados:

// src/hooks/useUserMutations.ts
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { User, CreateUserData, UpdateUserData } from '../types/user'
import { apiClient } from '../services/api'
const userService = {
create: async (data: CreateUserData): Promise<User> => {
const response = await apiClient.post('/users', data)
return response.data
},
update: async ({ id, data }: { id: string; data: UpdateUserData }): Promise<User> => {
const response = await apiClient.put(`/users/${id}`, data)
return response.data
},
delete: async (id: string): Promise<void> => {
await apiClient.delete(`/users/${id}`)
}
}
// Hook para criar usuário
export const useCreateUser = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: userService.create,
onSuccess: (newUser) => {
// Invalidar cache da lista de usuários
queryClient.invalidateQueries({ queryKey: ['users'] })
// Ou adicionar o novo usuário ao cache existente
queryClient.setQueryData<User[]>(['users'], (oldUsers) => {
return oldUsers ? [...oldUsers, newUser] : [newUser]
})
},
onError: (error) => {
console.error('Erro ao criar usuário:', error)
}
})
}
// Hook para atualizar usuário
export const useUpdateUser = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: userService.update,
onSuccess: (updatedUser) => {
// Atualizar cache do usuário específico
queryClient.setQueryData(['users', updatedUser.id], updatedUser)
// Atualizar na lista de usuários
queryClient.setQueryData<User[]>(['users'], (oldUsers) => {
return oldUsers?.map(user =>
user.id === updatedUser.id ? updatedUser : user
)
})
}
})
}
// Hook para deletar usuário
export const useDeleteUser = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: userService.delete,
onSuccess: (_, deletedId) => {
// Remover da lista
queryClient.setQueryData<User[]>(['users'], (oldUsers) => {
return oldUsers?.filter(user => user.id !== deletedId)
})
// Remover cache individual
queryClient.removeQueries({ queryKey: ['users', deletedId] })
}
})
}

4. Usando mutations nos componentes:

// src/components/CreateUserForm.tsx
import { FC, useState } from 'react'
import { useCreateUser } from '../hooks/useUserMutations'
import { CreateUserData } from '../types/user'
export const CreateUserForm: FC = () => {
const [formData, setFormData] = useState<CreateUserData>({
name: '',
email: '',
role: 'user'
})
const createUserMutation = useCreateUser()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
try {
await createUserMutation.mutateAsync(formData)
// Reset form após sucesso
setFormData({ name: '', email: '', role: 'user' })
// Mostrar sucesso
alert('Usuário criado com sucesso!')
} catch (error) {
// Erro já é tratado no hook
alert('Erro ao criar usuário')
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Nome"
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
disabled={createUserMutation.isPending}
/>
<input
type="email"
placeholder="Email"
value={formData.email}
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
disabled={createUserMutation.isPending}
/>
<select
value={formData.role}
onChange={(e) => setFormData(prev => ({ ...prev, role: e.target.value as any }))}
disabled={createUserMutation.isPending}
>
<option value="user">Usuário</option>
<option value="admin">Admin</option>
</select>
<button
type="submit"
disabled={createUserMutation.isPending}
>
{createUserMutation.isPending ? 'Criando...' : 'Criar Usuário'}
</button>
{createUserMutation.isError && (
<div className="error">
Erro: {createUserMutation.error?.message}
</div>
)}
</form>
)
}

Padrões Avançados

1. Optimistic Updates:

// src/hooks/useOptimisticUpdate.ts
import { useMutation, useQueryClient } from '@tanstack/react-query'
export const useUpdateUserOptimistic = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ id, data }: { id: string; data: UpdateUserData }) => {
return await userService.update({ id, data })
},
// Executar antes da mutation (otimistic)
onMutate: async ({ id, data }) => {
// Cancelar refetches em andamento
await queryClient.cancelQueries({ queryKey: ['users', id] })
// Snapshot do valor anterior
const previousUser = queryClient.getQueryData(['users', id])
// Otimistic update
queryClient.setQueryData(['users', id], (old: User) => ({
...old,
...data
}))
// Retornar context com dados anteriores
return { previousUser }
},
// Se der erro, reverter
onError: (err, variables, context) => {
if (context?.previousUser) {
queryClient.setQueryData(
['users', variables.id],
context.previousUser
)
}
},
// Sempre re-fetch após completar
onSettled: (data, error, variables) => {
queryClient.invalidateQueries({ queryKey: ['users', variables.id] })
}
})
}

2. Infinite Queries (Paginação):

// src/hooks/useInfiniteUsers.ts
import { useInfiniteQuery } from '@tanstack/react-query'
interface UsersPage {
users: User[]
nextCursor?: string
hasNextPage: boolean
}
const fetchUsers = async ({ pageParam = 0 }): Promise<UsersPage> => {
const response = await apiClient.get(`/users?page=${pageParam}&limit=20`)
return response.data
}
export const useInfiniteUsers = () => {
return useInfiniteQuery({
queryKey: ['users', 'infinite'],
queryFn: fetchUsers,
getNextPageParam: (lastPage) => {
return lastPage.hasNextPage ? lastPage.nextCursor : undefined
},
initialPageParam: 0,
})
}
// Componente com scroll infinito
export const InfiniteUserList: FC = () => {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading
} = useInfiniteUsers()
const allUsers = data?.pages.flatMap(page => page.users) ?? []
return (
<div>
{allUsers.map(user => (
<UserCard key={user.id} user={user} />
))}
{hasNextPage && (
<button
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage}
>
{isFetchingNextPage ? 'Carregando...' : 'Carregar mais'}
</button>
)}
{isLoading && <LoadingSpinner />}
</div>
)
}

3. Dependent Queries:

// src/hooks/useDependentQueries.ts
export const useUserWithPosts = (userId: string) => {
// Primeira query: buscar usuário
const userQuery = useQuery({
queryKey: ['users', userId],
queryFn: () => userService.getById(userId),
enabled: !!userId
})
// Segunda query: depende da primeira
const postsQuery = useQuery({
queryKey: ['posts', 'user', userId],
queryFn: () => postService.getByUserId(userId),
enabled: !!userQuery.data?.id // Só executa se user existir
})
return {
user: userQuery.data,
posts: postsQuery.data,
isLoading: userQuery.isLoading || postsQuery.isLoading,
error: userQuery.error || postsQuery.error
}
}

4. Background Sync:

// src/hooks/useBackgroundSync.ts
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useEffect } from 'react'
export const useBackgroundSync = () => {
const queryClient = useQueryClient()
useEffect(() => {
// Refetch críticos a cada 30 segundos
const interval = setInterval(() => {
queryClient.invalidateQueries({
queryKey: ['notifications'],
refetchType: 'active' // Só refetch queries ativas
})
}, 30000)
return () => clearInterval(interval)
}, [queryClient])
// Refetch ao reconectar
useEffect(() => {
const handleOnline = () => {
queryClient.invalidateQueries()
}
window.addEventListener('online', handleOnline)
return () => window.removeEventListener('online', handleOnline)
}, [queryClient])
}

Integração com Formulários

1. Com React Hook Form:

// src/components/UserFormWithQuery.tsx
import { useForm } from 'react-hook-form'
import { useUser, useUpdateUser } from '../hooks/useUsers'
interface UserFormProps {
userId: string
}
export const UserFormWithQuery: FC<UserFormProps> = ({ userId }) => {
const { data: user, isLoading } = useUser(userId)
const updateUserMutation = useUpdateUser()
const { register, handleSubmit, reset } = useForm<UpdateUserData>({
defaultValues: user
})
// Reset form quando dados carregarem
useEffect(() => {
if (user) {
reset(user)
}
}, [user, reset])
const onSubmit = async (data: UpdateUserData) => {
await updateUserMutation.mutateAsync({ id: userId, data })
}
if (isLoading) return <LoadingSpinner />
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register('name', { required: true })}
disabled={updateUserMutation.isPending}
/>
<input
{...register('email', { required: true })}
disabled={updateUserMutation.isPending}
/>
<button
type="submit"
disabled={updateUserMutation.isPending}
>
{updateUserMutation.isPending ? 'Salvando...' : 'Salvar'}
</button>
</form>
)
}

Configurações de Cache

1. Configuração por tipo de dado:

// src/lib/queryKeys.ts
export const queryKeys = {
users: {
all: ['users'] as const,
lists: () => [...queryKeys.users.all, 'list'] as const,
list: (filters: string) => [...queryKeys.users.lists(), { filters }] as const,
details: () => [...queryKeys.users.all, 'detail'] as const,
detail: (id: string) => [...queryKeys.users.details(), id] as const,
},
posts: {
all: ['posts'] as const,
byUser: (userId: string) => [...queryKeys.posts.all, 'user', userId] as const,
}
}
// Configurações específicas por entidade
export const cacheConfig = {
users: {
staleTime: 1000 * 60 * 5, // 5 minutos
gcTime: 1000 * 60 * 30, // 30 minutos
},
posts: {
staleTime: 1000 * 60 * 2, // 2 minutos
gcTime: 1000 * 60 * 10, // 10 minutos
}
}

2. Invalidação seletiva:

// src/utils/cacheUtils.ts
export const cacheUtils = {
invalidateUser: (queryClient: QueryClient, userId: string) => {
queryClient.invalidateQueries({ queryKey: queryKeys.users.detail(userId) })
queryClient.invalidateQueries({ queryKey: queryKeys.users.lists() })
},
invalidateUserPosts: (queryClient: QueryClient, userId: string) => {
queryClient.invalidateQueries({ queryKey: queryKeys.posts.byUser(userId) })
},
prefetchUser: (queryClient: QueryClient, userId: string) => {
queryClient.prefetchQuery({
queryKey: queryKeys.users.detail(userId),
queryFn: () => userService.getById(userId),
staleTime: cacheConfig.users.staleTime
})
}
}

Error Handling Global

// src/components/QueryErrorBoundary.tsx
import { QueryErrorResetBoundary } from '@tanstack/react-query'
import { ErrorBoundary } from 'react-error-boundary'
const ErrorFallback = ({ error, resetErrorBoundary }: any) => (
<div className="error-boundary">
<h2>Algo deu errado:</h2>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Tentar novamente</button>
</div>
)
export const QueryErrorBoundary: FC<{ children: ReactNode }> = ({ children }) => (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={reset}
>
{children}
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
)