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 v5npm 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.tsimport { 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 falharetry: 3,// Refetch quando a aba ganha focorefetchOnWindowFocus: false,// Refetch quando reconectarefetchOnReconnect: true,},mutations: {// Retry para mutationsretry: 1,}}})
2. Provider da aplicação:
// src/App.tsximport { 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.tsimport { useQuery } from '@tanstack/react-query'import { User } from '../types/user'import { apiClient } from '../services/api'// Service para APIconst 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áriosexport const useUsers = () => {return useQuery({queryKey: ['users'], // Chave única para cachequeryFn: userService.getAll,staleTime: 1000 * 60 * 5, // 5 minutos})}// Hook para usuário específicoexport 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 filtroexport const useUsersWithFilter = (filters: {search?: stringrole?: stringisActive?: boolean}) => {return useQuery({queryKey: ['users', filters],queryFn: () => userService.getFiltered(filters),// Reexecutar quando filtros mudaremenabled: Object.values(filters).some(Boolean),})}
2. Usando useQuery nos componentes:
// src/components/UserList.tsximport { 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 (<ErrorMessagemessage={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.tsimport { 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árioexport const useCreateUser = () => {const queryClient = useQueryClient()return useMutation({mutationFn: userService.create,onSuccess: (newUser) => {// Invalidar cache da lista de usuáriosqueryClient.invalidateQueries({ queryKey: ['users'] })// Ou adicionar o novo usuário ao cache existentequeryClient.setQueryData<User[]>(['users'], (oldUsers) => {return oldUsers ? [...oldUsers, newUser] : [newUser]})},onError: (error) => {console.error('Erro ao criar usuário:', error)}})}// Hook para atualizar usuárioexport const useUpdateUser = () => {const queryClient = useQueryClient()return useMutation({mutationFn: userService.update,onSuccess: (updatedUser) => {// Atualizar cache do usuário específicoqueryClient.setQueryData(['users', updatedUser.id], updatedUser)// Atualizar na lista de usuáriosqueryClient.setQueryData<User[]>(['users'], (oldUsers) => {return oldUsers?.map(user =>user.id === updatedUser.id ? updatedUser : user)})}})}// Hook para deletar usuárioexport const useDeleteUser = () => {const queryClient = useQueryClient()return useMutation({mutationFn: userService.delete,onSuccess: (_, deletedId) => {// Remover da listaqueryClient.setQueryData<User[]>(['users'], (oldUsers) => {return oldUsers?.filter(user => user.id !== deletedId)})// Remover cache individualqueryClient.removeQueries({ queryKey: ['users', deletedId] })}})}
4. Usando mutations nos componentes:
// src/components/CreateUserForm.tsximport { 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 sucessosetFormData({ name: '', email: '', role: 'user' })// Mostrar sucessoalert('Usuário criado com sucesso!')} catch (error) {// Erro já é tratado no hookalert('Erro ao criar usuário')}}return (<form onSubmit={handleSubmit}><inputtype="text"placeholder="Nome"value={formData.name}onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}disabled={createUserMutation.isPending}/><inputtype="email"placeholder="Email"value={formData.email}onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}disabled={createUserMutation.isPending}/><selectvalue={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><buttontype="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.tsimport { 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 andamentoawait queryClient.cancelQueries({ queryKey: ['users', id] })// Snapshot do valor anteriorconst previousUser = queryClient.getQueryData(['users', id])// Otimistic updatequeryClient.setQueryData(['users', id], (old: User) => ({...old,...data}))// Retornar context com dados anterioresreturn { previousUser }},// Se der erro, reverteronError: (err, variables, context) => {if (context?.previousUser) {queryClient.setQueryData(['users', variables.id],context.previousUser)}},// Sempre re-fetch após completaronSettled: (data, error, variables) => {queryClient.invalidateQueries({ queryKey: ['users', variables.id] })}})}
2. Infinite Queries (Paginação):
// src/hooks/useInfiniteUsers.tsimport { useInfiniteQuery } from '@tanstack/react-query'interface UsersPage {users: User[]nextCursor?: stringhasNextPage: 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 infinitoexport 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 && (<buttononClick={() => fetchNextPage()}disabled={isFetchingNextPage}>{isFetchingNextPage ? 'Carregando...' : 'Carregar mais'}</button>)}{isLoading && <LoadingSpinner />}</div>)}
3. Dependent Queries:
// src/hooks/useDependentQueries.tsexport const useUserWithPosts = (userId: string) => {// Primeira query: buscar usuárioconst userQuery = useQuery({queryKey: ['users', userId],queryFn: () => userService.getById(userId),enabled: !!userId})// Segunda query: depende da primeiraconst 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.tsimport { useQuery, useQueryClient } from '@tanstack/react-query'import { useEffect } from 'react'export const useBackgroundSync = () => {const queryClient = useQueryClient()useEffect(() => {// Refetch críticos a cada 30 segundosconst interval = setInterval(() => {queryClient.invalidateQueries({queryKey: ['notifications'],refetchType: 'active' // Só refetch queries ativas})}, 30000)return () => clearInterval(interval)}, [queryClient])// Refetch ao reconectaruseEffect(() => {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.tsximport { 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 carregaremuseEffect(() => {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}/><buttontype="submit"disabled={updateUserMutation.isPending}>{updateUserMutation.isPending ? 'Salvando...' : 'Salvar'}</button></form>)}
Configurações de Cache
1. Configuração por tipo de dado:
// src/lib/queryKeys.tsexport 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 entidadeexport const cacheConfig = {users: {staleTime: 1000 * 60 * 5, // 5 minutosgcTime: 1000 * 60 * 30, // 30 minutos},posts: {staleTime: 1000 * 60 * 2, // 2 minutosgcTime: 1000 * 60 * 10, // 10 minutos}}
2. Invalidação seletiva:
// src/utils/cacheUtils.tsexport 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.tsximport { 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 }) => (<ErrorBoundaryFallbackComponent={ErrorFallback}onReset={reset}>{children}</ErrorBoundary>)}</QueryErrorResetBoundary>)