<h2>Por que React Query Revolucionou o Gerenciamento de Estado</h2>
<p>Durante minha carreira, observei que a maior dificuldade dos desenvolvedores não está em aprender React, mas em <strong>gerenciar dados do servidor de forma eficiente</strong>. Antes do React Query, a maioria dos projetos acabava em um caos de estados locais, requisições duplicadas e sincronização manual de cache. O React Query (agora TanStack Query) resolveu isso de forma elegante, abstraindo toda a complexidade do cache, sincronização e refetch de dados.</p>
<p>Quando você integra TypeScript com React Query, ganha uma camada adicional de segurança: tipos inferidos automaticamente, autocomplete no IDE e erros detectados em tempo de compilação. Isso não é apenas conveniente—é a diferença entre um aplicativo robusto e um cheio de bugs sutis que aparecem em produção.</p>
<h2>Configuração Inicial e Entendimento de Queries</h2>
<h3>O que é uma Query?</h3>
<p>Uma Query no React Query é uma forma declarativa de buscar dados do servidor e mantê-los sincronizados com seu aplicativo. Diferentemente de um simples <code>fetch</code> em <code>useEffect</code>, uma Query cuida automaticamente de caching, refetch em background, garbage collection e muito mais. Você descreve <strong>onde</strong> buscar os dados e <strong>quando</strong> usá-los; o React Query gerencia o resto.</p>
<pre><code class="language-typescript">import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
interface User {
id: number;
name: string;
email: string;
}
// Função que faz a requisição
const fetchUser = async (userId: number): Promise<User> => {
const response = await axios.get(/api/users/${userId});
return response.data;
};
// Componente usando a Query
function UserProfile({ userId }: { userId: number }) {
const { data, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
if (isLoading) return <p>Carregando...</p>;
if (error) return <p>Erro: {error.message}</p>;
return <div>{data?.name} ({data?.email})</div>;
}</code></pre>
<p>Aqui, o React Query infere automaticamente que <code>data</code> tem o tipo <code>User | undefined</code>. O <code>queryKey</code> funciona como um identificador único e também como dependência—se <code>userId</code> muda, a Query refaz automaticamente.</p>
<h3>Configurando o QueryClient</h3>
<p>Toda aplicação React Query precisa de um <code>QueryClient</code> que centraliza a configuração global. Isso deve ser feito uma única vez, geralmente no arquivo raiz da aplicação.</p>
<pre><code class="language-typescript">import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 60 1000, // 5 minutos
gcTime: 10 60 1000, // 10 minutos (antes: cacheTime)
retry: 1,
refetchOnWindowFocus: true,
},
},
});
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<YourAppComponent />
</QueryClientProvider>
);
}</code></pre>
<p>O <code>staleTime</code> define quanto tempo os dados são considerados "frescos" sem precisar refetch. O <code>gcTime</code> (antigo <code>cacheTime</code>) define quando dados não usados são descartados. Esses valores devem ser ajustados conforme seu caso de uso.</p>
<h2>Mutations: Alterando Dados no Servidor</h2>
<h3>Entendendo Mutations com Tipos Seguros</h3>
<p>Se Queries são para <strong>ler</strong> dados, Mutations são para <strong>escrever</strong>. Uma Mutation é uma operação que modifica dados no servidor—criar, atualizar ou deletar. Com TypeScript, você controla tanto os dados enviados quanto a resposta esperada.</p>
<pre><code class="language-typescript">import { useMutation, useQueryClient } from '@tanstack/react-query';
interface CreateUserInput {
name: string;
email: string;
}
interface CreateUserResponse {
id: number;
name: string;
email: string;
createdAt: string;
}
const createUser = async (input: CreateUserInput): Promise<CreateUserResponse> => {
const response = await axios.post('/api/users', input);
return response.data;
};
function CreateUserForm() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: createUser,
onSuccess: (newUser) => {
// TypeScript sabe que newUser é CreateUserResponse
console.log('Usuário criado:', newUser.id, newUser.email);
// Invalida a Query de usuários para refetch automático
queryClient.invalidateQueries({ queryKey: ['users'] });
},
onError: (error: Error) => {
console.error('Erro ao criar:', error.message);
},
});
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
mutation.mutate({
name: formData.get('name') as string,
email: formData.get('email') as string,
});
};
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Nome" required />
<input name="email" type="email" placeholder="Email" required />
<button disabled={mutation.isPending}>
{mutation.isPending ? 'Criando...' : 'Criar Usuário'}
</button>
{mutation.isError && <p>Erro: {mutation.error.message}</p>}
</form>
);
}</code></pre>
<p>Observe como <code>mutation.mutate()</code> recebe um objeto tipado. Se você tentar passar um campo incorreto, TypeScript reclama antes do código executar. Além disso, no callback <code>onSuccess</code>, você sabe exatamente qual é o tipo de <code>newUser</code>.</p>
<h3>Atualizações Otimistas</h3>
<p>Uma das características mais poderosas do React Query é a <strong>atualização otimista</strong>: você atualiza a UI imediatamente, e se algo der errado, reverte para o estado anterior. Isso cria uma experiência de usuário fluida.</p>
<pre><code class="language-typescript">interface UpdateUserInput {
id: number;
name?: string;
email?: string;
}
const updateUser = async (input: UpdateUserInput): Promise<User> => {
const response = await axios.patch(/api/users/${input.id}, input);
return response.data;
};
function EditUserForm({ user }: { user: User }) {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: updateUser,
onMutate: async (newData) => {
// Cancela refetch em background para evitar conflito
await queryClient.cancelQueries({ queryKey: ['user', user.id] });
// Salva o estado anterior
const previousUser = queryClient.getQueryData<User>(['user', user.id]);
// Atualiza cache otimisticamente
queryClient.setQueryData(['user', user.id], {
...user,
...newData,
});
return { previousUser };
},
onError: (error, variables, context) => {
// Se houver erro, reverte para o anterior
if (context?.previousUser) {
queryClient.setQueryData(['user', user.id], context.previousUser);
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['user', user.id] });
},
});
return (
<button
onClick={() => mutation.mutate({ id: user.id, name: 'Novo Nome' })}
disabled={mutation.isPending}
>
Atualizar
</button>
);
}</code></pre>
<p>O fluxo aqui é: 1) <code>onMutate</code> atualiza a UI imediatamente, 2) a requisição é enviada, 3) se suceder, valida os dados; se falhar, reverte com <code>onError</code>.</p>
<h2>Tipos Inferidos e Padrões Avançados</h2>
<h3>Inferência Automática com TypeScript</h3>
<p>Uma das maravilhas de usar React Query com TypeScript é que você pode deixar o TypeScript <strong>inferir</strong> tipos de funções simples, reduzindo boilerplate. A chave é estruturar suas funções de forma clara.</p>
<pre><code class="language-typescript">// Essa função retorna User[], TypeScript infere automaticamente
async function fetchUsers() {
const response = await axios.get<User[]>('/api/users');
return response.data;
}
// Aqui data será tipado como User[] sem precisar declarar manualmente
const { data: users } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers, // Tipo inferido
});
// Se você quiser ser explícito (recomendado em funções complexas):
const { data: usersExplicit } = useQuery<User[]>({
queryKey: ['users'],
queryFn: fetchUsers,
});</code></pre>
<p>Em projetos reais, é comum criar um padrão wrapper para todas as requisições. Assim centralizamos a lógica de erro e autenticação:</p>
<pre><code class="language-typescript">// api.ts - seu cliente HTTP centralizado
import axios, { AxiosError } from 'axios';
interface ApiError {
code: string;
message: string;
}
const apiClient = axios.create({
baseURL: process.env.REACT_APP_API_URL,
headers: {
'Content-Type': 'application/json',
},
});
apiClient.interceptors.response.use(
response => response,
error => {
const apiError: ApiError = {
code: error.response?.data?.code | | 'UNKNOWN_ERROR', message: error.response?.data?.message || error.message,
};
return Promise.reject(apiError);
}
);
export const api = {
getUser: (id: number) => apiClient.get<User>(/users/${id}).then(r => r.data),
getUsers: () => apiClient.get<User[]>('/users').then(r => r.data),
createUser: (data: CreateUserInput) =>
apiClient.post<CreateUserResponse>('/users', data).then(r => r.data),
updateUser: (id: number, data: Partial<User>) =>
apiClient.patch<User>(/users/${id}, data).then(r => r.data),
};
// Componente usando
function UsersList() {
const { data: users } = useQuery({
queryKey: ['users'],
queryFn: api.getUsers,
});
return <ul>{users?.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}</code></pre>
<h3>Paginação e Queries Dinâmicas</h3>
<p>Paginar com React Query é trivial quando você estrutura bem. A chave é incluir a página no <code>queryKey</code>.</p>
<pre><code class="language-typescript">interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
limit: number;
}
function UsersList() {
const [page, setPage] = useState(1);
const { data, isLoading } = useQuery({
queryKey: ['users', page], // page é parte da chave!
queryFn: () =>
api.getUsers({ page, limit: 10 })
.then(r => r as PaginatedResponse<User>),
});
return (
<>
{data?.data.map(u => <div key={u.id}>{u.name}</div>)}
<button onClick={() => setPage(p => p + 1)} disabled={isLoading}>
Próxima
</button>
</>
);
}</code></pre>
<p>Quando <code>page</code> muda, React Query automaticamente faz uma nova requisição. O cache anterior permanece, então se o usuário voltar para a página 1, os dados já estão prontos.</p>
<h3>Usando <code>useQueries</code> para Múltiplas Queries</h3>
<p>Às vezes você precisa buscar vários recursos. O hook <code>useQueries</code> permite isso de forma eficiente:</p>
<pre><code class="language-typescript">function UserDashboard({ userIds }: { userIds: number[] }) {
const queries = useQueries({
queries: userIds.map(id => ({
queryKey: ['user', id],
queryFn: () => api.getUser(id),
})),
});
// queries é um array onde cada item é { data, isLoading, error }
const allLoading = queries.some(q => q.isLoading);
const users = queries.map(q => q.data).filter(Boolean);
if (allLoading) return <p>Carregando...</p>;
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}</code></pre>
<p>Isso é muito mais eficiente que múltiplos <code>useQuery</code> separados, pois o React Query otimiza as requisições.</p>
<h2>Boas Práticas e Padrões de Produção</h2>
<h3>Tratamento de Erros com Tipos Customizados</h3>
<p>Em uma aplicação real, seus erros de API seguem um padrão. TypeScript permite tipá-los corretamente:</p>
<pre><code class="language-typescript">interface ApiErrorResponse {
error: {
code: string;
message: string;
details?: Record<string, string>;
};
}
async function fetchUserSafe(id: number): Promise<User> {
try {
const response = await apiClient.get<User>(/users/${id});
return response.data;
} catch (err) {
const error = err as AxiosError<ApiErrorResponse>;
throw new Error(error.response?.data?.error?.message || 'Erro desconhecido');
}
}
function UserProfile({ userId }: { userId: number }) {
const { data, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUserSafe(userId),
});
if (error instanceof Error) {
return <p>Erro: {error.message}</p>;
}
return <div>{data?.name}</div>;
}</code></pre>
<h3>DevTools para Debug</h3>
<p>React Query fornece DevTools que facilitam muito o debug. Instale e use assim:</p>
<pre><code class="language-typescript">import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
const queryClient = new QueryClient();
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}</code></pre>
<p>Os DevTools mostram todas as Queries ativas, seu estado, histórico de requisições e permitem refetch manual. Indispensável para desenvolvimento.</p>
<h2>Conclusão</h2>
<p>Dominando React Query com TypeScript, você alcança três marcos importantes: <strong>1) Segurança de tipos desde a busca até a exibição de dados</strong>, eliminando a maioria dos erros silenciosos que ocorrem em runtime; <strong>2) Código mais limpo e declarativo</strong>, onde você descreve o que precisa (uma Query) e o framework cuida de como buscar, cachear e sincronizar; <strong>3) Experiências de usuário superiores</strong> com atualizações otimistas, paginação automática e refetch inteligente, tudo com pouco código.</p>
<p>O React Query não é apenas uma biblioteca—é uma mudança de paradigma em como pensamos sobre estado remoto. Quando você internaliza seus conceitos, nunca mais volta a gerenciar requisições manualmente.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://tanstack.com/query/latest" target="_blank" rel="noopener noreferrer">TanStack Query Official Documentation</a></li>
<li><a href="https://tanstack.com/query/latest/docs/react/typescript" target="_blank" rel="noopener noreferrer">React Query TypeScript Guide</a></li>
<li><a href="https://axios-http.com/docs/typescript" target="_blank" rel="noopener noreferrer">Axios TypeScript Support</a></li>
<li><a href="https://epicreact.dev/" target="_blank" rel="noopener noreferrer">Kent C. Dodds - React Query Course</a></li>
<li><a href="https://github.com/TanStack/query" target="_blank" rel="noopener noreferrer">TanStack Query GitHub Repository</a></li>
</ul>
<p><!-- FIM --></p>