TanStack Query es la solución estándar para la gestión de estado del servidor en aplicaciones React.
useQuery: obtener y cachear datos
useQuery se suscribe a un estado del servidor con queryKey como clave de caché.
import { useQuery, QueryClient, QueryClientProvider } from '@tanstack/react-query';
// Setup: wrap your app
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 30, // 30 minutes (formerly cacheTime)
retry: 3,
refetchOnWindowFocus: true,
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<MyApp />
</QueryClientProvider>
);
}
// Basic query
interface User {
id: number;
name: string;
email: string;
}
function useUser(userId: number) {
return useQuery<User>({
queryKey: ['user', userId], // cache key — must be unique
queryFn: async () => {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error('Failed to fetch user');
return res.json();
},
enabled: userId > 0, // only run if userId is valid
staleTime: 1000 * 60 * 10, // override global default
});
}
function UserProfile({ userId }: { userId: number }) {
const { data, isLoading, isError, error } = useUser(userId);
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error: {error.message}</div>;
return <div>{data.name} — {data.email}</div>;
}useMutation: crear, actualizar, eliminar
useMutation maneja operaciones CRUD con callbacks onSuccess.
import { useMutation, useQueryClient } from '@tanstack/react-query';
interface CreatePostInput {
title: string;
body: string;
userId: number;
}
function useCreatePost() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (input: CreatePostInput) => {
const res = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input),
});
if (!res.ok) throw new Error('Failed to create post');
return res.json();
},
onSuccess: (newPost) => {
// Invalidate and refetch posts list
queryClient.invalidateQueries({ queryKey: ['posts'] });
// Or add directly to cache (no refetch needed)
queryClient.setQueryData(['post', newPost.id], newPost);
},
onError: (error) => {
console.error('Failed to create post:', error);
},
});
}
function CreatePostForm() {
const mutation = useCreatePost();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
mutation.mutate({ title: 'New Post', body: 'Content', userId: 1 });
};
return (
<form onSubmit={handleSubmit}>
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Creating...' : 'Create Post'}
</button>
{mutation.isError && <p>Error: {mutation.error.message}</p>}
{mutation.isSuccess && <p>Post created!</p>}
</form>
);
}Actualizaciones optimistas para UI instantánea
Las actualizaciones optimistas aplican cambios antes de la respuesta del servidor.
import { useMutation, useQueryClient } from '@tanstack/react-query';
interface Todo {
id: number;
title: string;
completed: boolean;
}
function useToggleTodo() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (todo: Todo) =>
fetch(`/api/todos/${todo.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ completed: !todo.completed }),
}).then(r => r.json()),
// Optimistic update — runs BEFORE the mutation
onMutate: async (updatedTodo) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Snapshot current data for rollback
const previousTodos = queryClient.getQueryData<Todo[]>(['todos']);
// Optimistically update the cache
queryClient.setQueryData<Todo[]>(['todos'], (old) =>
old?.map(todo =>
todo.id === updatedTodo.id
? { ...todo, completed: !todo.completed }
: todo
)
);
return { previousTodos }; // context for onError
},
// If mutation fails, roll back to snapshot
onError: (_err, _variables, context) => {
if (context?.previousTodos) {
queryClient.setQueryData(['todos'], context.previousTodos);
}
},
// Always refetch after error or success
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
}Consultas infinitas para paginación
useInfiniteQuery maneja paginación basada en cursor.
import { useInfiniteQuery } from '@tanstack/react-query';
interface Page {
items: Post[];
nextCursor?: string;
}
function useInfinitePosts() {
return useInfiniteQuery<Page>({
queryKey: ['posts', 'infinite'],
queryFn: async ({ pageParam }) => {
const url = pageParam
? `/api/posts?cursor=${pageParam}`
: '/api/posts';
return fetch(url).then(r => r.json());
},
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
}
function InfinitePostList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfinitePosts();
return (
<div>
{data?.pages.flatMap(page => page.items).map(post => (
<div key={post.id}>{post.title}</div>
))}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? 'Loading...' : 'Load more'}
</button>
)}
</div>
);
}Patrones de claves de consulta y prefetching
Las factories de claves consistentes previenen errores de caché.
// Query key patterns — factory functions for consistency
const queryKeys = {
all: ['posts'] as const,
lists: () => [...queryKeys.all, 'list'] as const,
list: (filters: string) => [...queryKeys.lists(), { filters }] as const,
details: () => [...queryKeys.all, 'detail'] as const,
detail: (id: number) => [...queryKeys.details(), id] as const,
};
// Usage:
useQuery({ queryKey: queryKeys.detail(1), queryFn: ... });
// Invalidate all posts:
queryClient.invalidateQueries({ queryKey: queryKeys.all });
// Invalidate only lists:
queryClient.invalidateQueries({ queryKey: queryKeys.lists() });
// Prefetching for better UX
async function prefetchUser(userId: number) {
await queryClient.prefetchQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 10 * 60 * 1000, // only prefetch if > 10 min stale
});
}
// On hover: prefetch before click
<link onMouseEnter={() => prefetchUser(userId)} href={`/users/${userId}`}>
View Profile
</link>TanStack Query vs otras soluciones
| Feature | TanStack Query | Redux Toolkit | SWR | Apollo |
|---|---|---|---|---|
| Caching | Built-in (staleTime, gcTime) | Manual | Built-in | Built-in (Apollo-only) |
| Optimistic updates | First-class | Manual | Basic | First-class |
| Infinite query | useInfiniteQuery | Manual | useSWRInfinite | fetchMore |
| Mutations | useMutation | RTK Query | No (use fetch) | useMutation |
| DevTools | Excellent | Excellent | Basic | Good |
| REST + GraphQL | Both | Both | REST focused | GraphQL only |
Mejores prácticas
- Usar factories de claves de consulta para consistencia.
- Establecer staleTime según los requisitos de frescura de datos.
- Siempre proporcionar estados de carga y error.
- Usar actualizaciones optimistas para operaciones rápidas.
- Precargar datos en hover o transiciones de ruta.
FAQ
¿Diferencia entre staleTime y gcTime?
staleTime controla cuándo los datos son obsoletos, gcTime cuánto tiempo se mantienen en memoria.
¿Cómo maneja TanStack Query las condiciones de carrera?
Deduplica automáticamente las solicitudes idénticas.
¿TanStack Query con Redux o Zustand?
Complementarios: TanStack Query para estado del servidor, Zustand para estado del cliente.
¿Manejar errores de autenticación globalmente?
Usar el callback onError global del QueryClient.
¿Cambios en TanStack Query v5?
cacheTime renombrado a gcTime, parámetro queryFn como objeto, soporte RSC mejorado.