C
|Caroline Oliveira
Tutoriais

Migrando react-router-dom para TanStack router

Data de publicação

Ilustração estilizada e futurista em tons de roxo, rosa e azul, mostrando mãos robóticas digitando sobre um tablet ou tela interativa

Visão geral

Usar react-router-dom e TanStack Query (React Query) no mesmo projeto sempre me gerou atrito. O roteador não conhece o queryClient; não há loaders que se comuniquem com o cache. Para ter prefetch na navegação eu teria que chamar queryClient.prefetchQuery manualmente em useEffect ou em event handlers de Link, repetindo queryKeys e queryFns. Loading e erro ficavam só no componente, e o cache do Query e o “dado da rota” viviam separados — mesma requisição, duas fontes de verdade.

Por isso decidi migrar um projeto pessoal para o TanStack Router. Preferi ele principalmente pela facilidade de integração com o Query, pela remoção das lógicas manuais dentro dos componentes (aqueles if (isLoading), if (error), if (!data) em toda página) e por conseguir tratar o cache de forma eficiente em conjunto com o roteamento.

A mudança principal foi trocar o react-router-dom por um roteador da mesma família do Query, com integração de entre as libs: o router recebe o queryClient no context, e cada rota pode ter um loader que usa os mesmos queryOptions que o useQuery usa nos componentes. Assim, o loader preenche o cache antes da tela montar; qualquer componente que use useQuery com esses options (ou useLoaderData()) consome o mesmo dado, sem duplicar lógica.



Benefícios na prática

  • Uma única definição de “como buscar”: queryOptions() com queryKey e queryFn é usado no loader (via ensureQueryData) e no hook (useQuery). Zero duplicação.
  • Prefetch automático na navegação: ao seguir um link, o loader roda antes da transição; o cache já está quente quando o componente monta.
  • Loading alinhado ao router: pendingComponent e pendingMs fazem o router mostrar skeleton enquanto o loader resolve; não preciso orquestrar isso no componente.
  • 404 e erros na rota: no loader posso usar throw notFound() e notFoundComponent na rota; o mesmo queryClient que busca os dados pode ser usado para validar existência.
  • Type-safety: params da rota (params.id) e o retorno do loader são tipados; o useLoaderData() da rota herda esse tipo.

Código: Antes (react-router-dom) x Depois (TanStack Router)

Antes — react-router-dom

Typescript
import { BrowserRouter, Routes, Route } from "react-router-dom";
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/item/:id" element={<ItemDetailPage />} />
</Routes>
</BrowserRouter>
);
}
// ItemDetailPage.tsx — fetch só no componente, loading só aqui
import { useParams } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
function itemQuery(id: string) {
return fetch(`/api/items/${id}`).then((r) => r.json());
}
export function ItemDetailPage() {
const { id } = useParams<{ id: string }>();
const { data, isLoading, error } = useQuery({
queryKey: ["item", id],
queryFn: () => itemQuery(id!),
enabled: Boolean(id),
});
if (isLoading) return <ItemSkeleton />;
if (error) return <div>Erro ao carregar</div>;
if (!data) return null;
return (
<div>
<h1>{data.title}</h1>
<p>{data.description}</p>
</div>
);
}

Problemas: roteador e cache desacoplados; sem prefetch na navegação; queryKey/queryFn só no componente; loading e erro tratados manualmente na página.

Depois — TanStack Router

  1. Definição das rotas:
Typescript
// rootRoute.tsx — context com queryClient
import { QueryClient } from "@tanstack/react-query";
import { createRootRouteWithContext, Outlet } from "@tanstack/react-router";
export interface RouterContext {
queryClient: QueryClient;
}
export const rootRoute = createRootRouteWithContext<RouterContext>()({
component: () => <Outlet />,
});
// routeTree.tsx — router recebe o queryClient
import { createRouter } from "@tanstack/react-router";
import { queryClient } from "@/lib/query/queryClient";
import { rootRoute } from "./rootRoute";
import { itemDetailRoute } from "./routes/item";
const routeTree = rootRoute.addChildren([itemDetailRoute]);
export const router = createRouter({
routeTree,
context: { queryClient },
});
  1. Hook
Typescript
// useGetItem.ts — queryOptions compartilhados (loader + useQuery)
import { useQuery, queryOptions } from "@tanstack/react-query";
export function itemQueryOptions(id: string) {
return queryOptions({
queryKey: ["item", id],
queryFn: () => fetch(`/api/items/${id}`).then((r) => r.json()),
});
}
export function useGetItem(id: string) {
return useQuery({ ...itemQueryOptions(id), enabled: Boolean(id) });
}
  1. Componentes que compõe a rota
Typescript
// routes/item.tsx — loader usa o mesmo queryOptions
import { createRoute, notFound } from "@tanstack/react-router";
import { itemQueryOptions } from "@/lib/query/useGetItem";
import { rootRoute } from "@/lib/router/rootRoute";
import { ItemDetailPage } from "@/components/pages/ItemDetailPage";
import { ItemSkeleton } from "@/components/ItemSkeleton";
export const itemDetailRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/item/$id",
component: ItemDetailPage,
pendingComponent: ItemSkeleton,
pendingMs: 200,
loader: async ({ context: { queryClient }, params }) => {
const data = await queryClient.ensureQueryData(itemQueryOptions(params.id));
if (!data) throw notFound();
return data;
},
notFoundComponent: () => <h1>Item não encontrado</h1>,
});

No código acima, tem a configuração básica para os estados de loading, erro e sucesso. Sem precisar de manipulação direta no componente da página propriamente dito.

  1. Página do item
Typescript
// ItemDetailPage.tsx — só consome dado já no cache
import { itemDetailRoute } from "@/lib/router/routeTree";
export function ItemDetailPage() {
const data = itemDetailRoute.useLoaderData();
return (
<div>
<h1>{data.title}</h1>
<p>{data.description}</p>
</div>
);
}


Vantagens: uma única definição (itemQueryOptions); prefetch no loader; loading via pendingComponent; 404 via notFound(); componente sem isLoading/error; mesmo cache se outro componente usar useGetItem(id).

Toda a lógica de como a página vai se comportar fica centralizada em um único arquivo e sem a necessidade de fazer vários itens encadeados.

Se você busca praticidade na hora de definir uma rota e quer evitar surpresas de como a página vai funcionar em caso de falha de requisição ou estado de loading, vale a pena dar uma chance a essa dupla. Estou usando em um projeto pessoal e até agora não houveram surpresas, além da curva de aprendizagem ser bem rápida e com uma documentação bem completa.