TypeScript

Boas Práticas de React Query com TypeScript: Queries, Mutations e Tipos Inferidos para Times Ágeis

14 min de leitura

Boas Práticas de React Query com TypeScript: Queries, Mutations e Tipos Inferidos para Times Ágeis

Por que React Query Revolucionou o Gerenciamento de Estado Durante minha carreira, observei que a maior dificuldade dos desenvolvedores não está em aprender React, mas em gerenciar dados do servidor de forma eficiente. 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. 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. Configuração Inicial e Entendimento de Queries O que é uma Query? Uma Query no React Query é uma forma declarativa de buscar dados do servidor e mantê-los sincronizados com seu aplicativo. Diferentemente de um simples em

<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 &#039;@tanstack/react-query&#039;;

import axios from &#039;axios&#039;;

interface User {

id: number;

name: string;

email: string;

}

// Função que faz a requisição

const fetchUser = async (userId: number): Promise&lt;User&gt; =&gt; {

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: [&#039;user&#039;, userId],

queryFn: () =&gt; fetchUser(userId),

});

if (isLoading) return &lt;p&gt;Carregando...&lt;/p&gt;;

if (error) return &lt;p&gt;Erro: {error.message}&lt;/p&gt;;

return &lt;div&gt;{data?.name} ({data?.email})&lt;/div&gt;;

}</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 &#039;@tanstack/react-query&#039;;

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 (

&lt;QueryClientProvider client={queryClient}&gt;

&lt;YourAppComponent /&gt;

&lt;/QueryClientProvider&gt;

);

}</code></pre>

<p>O <code>staleTime</code> define quanto tempo os dados são considerados &quot;frescos&quot; 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 &#039;@tanstack/react-query&#039;;

interface CreateUserInput {

name: string;

email: string;

}

interface CreateUserResponse {

id: number;

name: string;

email: string;

createdAt: string;

}

const createUser = async (input: CreateUserInput): Promise&lt;CreateUserResponse&gt; =&gt; {

const response = await axios.post(&#039;/api/users&#039;, input);

return response.data;

};

function CreateUserForm() {

const queryClient = useQueryClient();

const mutation = useMutation({

mutationFn: createUser,

onSuccess: (newUser) =&gt; {

// TypeScript sabe que newUser é CreateUserResponse

console.log(&#039;Usuário criado:&#039;, newUser.id, newUser.email);

// Invalida a Query de usuários para refetch automático

queryClient.invalidateQueries({ queryKey: [&#039;users&#039;] });

},

onError: (error: Error) =&gt; {

console.error(&#039;Erro ao criar:&#039;, error.message);

},

});

const handleSubmit = (e: React.FormEvent&lt;HTMLFormElement&gt;) =&gt; {

e.preventDefault();

const formData = new FormData(e.currentTarget);

mutation.mutate({

name: formData.get(&#039;name&#039;) as string,

email: formData.get(&#039;email&#039;) as string,

});

};

return (

&lt;form onSubmit={handleSubmit}&gt;

&lt;input name=&quot;name&quot; placeholder=&quot;Nome&quot; required /&gt;

&lt;input name=&quot;email&quot; type=&quot;email&quot; placeholder=&quot;Email&quot; required /&gt;

&lt;button disabled={mutation.isPending}&gt;

{mutation.isPending ? &#039;Criando...&#039; : &#039;Criar Usuário&#039;}

&lt;/button&gt;

{mutation.isError &amp;&amp; &lt;p&gt;Erro: {mutation.error.message}&lt;/p&gt;}

&lt;/form&gt;

);

}</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&lt;User&gt; =&gt; {

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) =&gt; {

// Cancela refetch em background para evitar conflito

await queryClient.cancelQueries({ queryKey: [&#039;user&#039;, user.id] });

// Salva o estado anterior

const previousUser = queryClient.getQueryData&lt;User&gt;([&#039;user&#039;, user.id]);

// Atualiza cache otimisticamente

queryClient.setQueryData([&#039;user&#039;, user.id], {

...user,

...newData,

});

return { previousUser };

},

onError: (error, variables, context) =&gt; {

// Se houver erro, reverte para o anterior

if (context?.previousUser) {

queryClient.setQueryData([&#039;user&#039;, user.id], context.previousUser);

}

},

onSuccess: () =&gt; {

queryClient.invalidateQueries({ queryKey: [&#039;user&#039;, user.id] });

},

});

return (

&lt;button

onClick={() =&gt; mutation.mutate({ id: user.id, name: &#039;Novo Nome&#039; })}

disabled={mutation.isPending}

&gt;

Atualizar

&lt;/button&gt;

);

}</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&lt;User[]&gt;(&#039;/api/users&#039;);

return response.data;

}

// Aqui data será tipado como User[] sem precisar declarar manualmente

const { data: users } = useQuery({

queryKey: [&#039;users&#039;],

queryFn: fetchUsers, // Tipo inferido

});

// Se você quiser ser explícito (recomendado em funções complexas):

const { data: usersExplicit } = useQuery&lt;User[]&gt;({

queryKey: [&#039;users&#039;],

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 &#039;axios&#039;;

interface ApiError {

code: string;

message: string;

}

const apiClient = axios.create({

baseURL: process.env.REACT_APP_API_URL,

headers: {

&#039;Content-Type&#039;: &#039;application/json&#039;,

},

});

apiClient.interceptors.response.use(

response =&gt; response,

error =&gt; {

const apiError: ApiError = {

code: error.response?.data?.code | | &#039;UNKNOWN_ERROR&#039;, message: error.response?.data?.message || error.message,

};

return Promise.reject(apiError);

}

);

export const api = {

getUser: (id: number) =&gt; apiClient.get&lt;User&gt;(/users/${id}).then(r =&gt; r.data),

getUsers: () =&gt; apiClient.get&lt;User[]&gt;(&#039;/users&#039;).then(r =&gt; r.data),

createUser: (data: CreateUserInput) =&gt;

apiClient.post&lt;CreateUserResponse&gt;(&#039;/users&#039;, data).then(r =&gt; r.data),

updateUser: (id: number, data: Partial&lt;User&gt;) =&gt;

apiClient.patch&lt;User&gt;(/users/${id}, data).then(r =&gt; r.data),

};

// Componente usando

function UsersList() {

const { data: users } = useQuery({

queryKey: [&#039;users&#039;],

queryFn: api.getUsers,

});

return &lt;ul&gt;{users?.map(u =&gt; &lt;li key={u.id}&gt;{u.name}&lt;/li&gt;)}&lt;/ul&gt;;

}</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&lt;T&gt; {

data: T[];

total: number;

page: number;

limit: number;

}

function UsersList() {

const [page, setPage] = useState(1);

const { data, isLoading } = useQuery({

queryKey: [&#039;users&#039;, page], // page é parte da chave!

queryFn: () =&gt;

api.getUsers({ page, limit: 10 })

.then(r =&gt; r as PaginatedResponse&lt;User&gt;),

});

return (

&lt;&gt;

{data?.data.map(u =&gt; &lt;div key={u.id}&gt;{u.name}&lt;/div&gt;)}

&lt;button onClick={() =&gt; setPage(p =&gt; p + 1)} disabled={isLoading}&gt;

Próxima

&lt;/button&gt;

&lt;/&gt;

);

}</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 =&gt; ({

queryKey: [&#039;user&#039;, id],

queryFn: () =&gt; api.getUser(id),

})),

});

// queries é um array onde cada item é { data, isLoading, error }

const allLoading = queries.some(q =&gt; q.isLoading);

const users = queries.map(q =&gt; q.data).filter(Boolean);

if (allLoading) return &lt;p&gt;Carregando...&lt;/p&gt;;

return &lt;ul&gt;{users.map(u =&gt; &lt;li key={u.id}&gt;{u.name}&lt;/li&gt;)}&lt;/ul&gt;;

}</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&lt;string, string&gt;;

};

}

async function fetchUserSafe(id: number): Promise&lt;User&gt; {

try {

const response = await apiClient.get&lt;User&gt;(/users/${id});

return response.data;

} catch (err) {

const error = err as AxiosError&lt;ApiErrorResponse&gt;;

throw new Error(error.response?.data?.error?.message || &#039;Erro desconhecido&#039;);

}

}

function UserProfile({ userId }: { userId: number }) {

const { data, error } = useQuery({

queryKey: [&#039;user&#039;, userId],

queryFn: () =&gt; fetchUserSafe(userId),

});

if (error instanceof Error) {

return &lt;p&gt;Erro: {error.message}&lt;/p&gt;;

}

return &lt;div&gt;{data?.name}&lt;/div&gt;;

}</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 &#039;@tanstack/react-query-devtools&#039;;

import { QueryClientProvider, QueryClient } from &#039;@tanstack/react-query&#039;;

const queryClient = new QueryClient();

export default function App() {

return (

&lt;QueryClientProvider client={queryClient}&gt;

&lt;YourApp /&gt;

&lt;ReactQueryDevtools initialIsOpen={false} /&gt;

&lt;/QueryClientProvider&gt;

);

}</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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em TypeScript

Observabilidade com TypeScript: OpenTelemetry e Tipos de Trace na Prática
Observabilidade com TypeScript: OpenTelemetry e Tipos de Trace na Prática

O que é Observabilidade e por que OpenTelemetry? Observabilidade é a capacida...

Guia Completo de CLI com TypeScript: Construindo Ferramentas de Linha de Comando Tipadas
Guia Completo de CLI com TypeScript: Construindo Ferramentas de Linha de Comando Tipadas

Introdução: Por que TypeScript em CLIs? Quando você desenvolve aplicações de...

TypeScript Compiler API: Parsear, Transformar e Gerar Código na Prática
TypeScript Compiler API: Parsear, Transformar e Gerar Código na Prática

Introdução à TypeScript Compiler API A TypeScript Compiler API é um conjunto...