TanStack Query (React Query)
Potente gestión de estado del servidor con caché, actualizaciones en segundo plano y más
TanStack Query - Gestión de Estado del Servidor
TanStack Query (anteriormente React Query) es una poderosa biblioteca de obtención de datos y gestión de estado del servidor. Maneja el caché, actualizaciones en segundo plano, datos obsoletos, paginación y más desde el principio. Está específicamente diseñada para gestionar estado del servidor - datos que viven en tu backend.
Características Clave
- Caché Automático — Cachea respuestas y reduce llamadas a API
- Actualizaciones en Segundo Plano — Revalida datos obsoletos automáticamente
- Deduplicación de Solicitudes — Deduplica solicitudes concurrentes idénticas
- Actualizaciones Optimistas — Actualiza la UI antes de que el servidor confirme
- Consultas Infinitas — Paginación y scroll infinito fácil
Instalación
npm install @tanstack/react-query
Configuración
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
// Crea un cliente
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 1 minuto
retry: 3,
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<MyApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
Consulta Básica
import { useQuery } from '@tanstack/react-query';
// Función de obtención
async function fetchTodos() {
const response = await fetch('/api/todos');
if (!response.ok) throw new Error('Error al obtener');
return response.json();
}
function TodoList() {
const { data, isLoading, isError, error, refetch } = useQuery({
queryKey: ['todos'], // Clave única para caché
queryFn: fetchTodos, // Función que devuelve una promesa
});
if (isLoading) return <div>Cargando...</div>;
if (isError) return <div>Error: {error.message}</div>;
return (
<div>
<button onClick={() => refetch()}>Actualizar</button>
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
}
Consulta con Parámetros
import { useQuery } from '@tanstack/react-query';
async function fetchUser(userId) {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
function UserProfile({ userId }) {
const { data: user, isLoading } = useQuery({
queryKey: ['user', userId], // La clave incluye el parámetro
queryFn: () => fetchUser(userId),
enabled: !!userId, // Solo ejecuta si existe userId
});
if (isLoading) return <div>Cargando...</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
// Consultas dependientes
function UserPosts({ userId }) {
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
const { data: posts } = useQuery({
queryKey: ['posts', user?.id],
queryFn: () => fetchPosts(user.id),
enabled: !!user?.id, // Solo obtiene cuando el usuario está cargado
});
return (/* ... */);
}
Mutaciones
import { useMutation, useQueryClient } from '@tanstack/react-query';
async function createTodo(newTodo) {
const response = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newTodo),
});
return response.json();
}
function AddTodo() {
const queryClient = useQueryClient();
const [text, setText] = useState('');
const mutation = useMutation({
mutationFn: createTodo,
onSuccess: () => {
// Invalida y revalida todos
queryClient.invalidateQueries({ queryKey: ['todos'] });
setText('');
},
onError: (error) => {
console.error('Error al crear tarea:', error);
},
});
const handleSubmit = (e) => {
e.preventDefault();
mutation.mutate({ text, completed: false });
};
return (
<form onSubmit={handleSubmit}>
<input
value={text}
onChange={(e) => setText(e.target.value)}
disabled={mutation.isPending}
/>
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Agregando...' : 'Agregar Tarea'}
</button>
</form>
);
}
Actualizaciones Optimistas
import { useMutation, useQueryClient } from '@tanstack/react-query';
function TodoItem({ todo }) {
const queryClient = useQueryClient();
const toggleMutation = useMutation({
mutationFn: (todoId) => fetch(`/api/todos/${todoId}/toggle`, { method: 'PATCH' }),
// Actualización optimista
onMutate: async (todoId) => {
// Cancela revalidaciones salientes
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Captura valor anterior
const previousTodos = queryClient.getQueryData(['todos']);
// Actualiza optimistamente
queryClient.setQueryData(['todos'], (old) =>
old.map(t => t.id === todoId ? { ...t, completed: !t.completed } : t)
);
// Devuelve contexto con captura
return { previousTodos };
},
// Si falla la mutación, revertir
onError: (err, todoId, context) => {
queryClient.setQueryData(['todos'], context.previousTodos);
},
// Siempre revalida después de error o éxito
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
return (
<li onClick={() => toggleMutation.mutate(todo.id)}>
{todo.completed ? '✓' : '○'} {todo.text}
</li>
);
}
Consultas Infinitas
import { useInfiniteQuery } from '@tanstack/react-query';
async function fetchPosts({ pageParam = 1 }) {
const response = await fetch(`/api/posts?page=${pageParam}&limit=10`);
return response.json();
}
function InfinitePostList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
getNextPageParam: (lastPage, pages) => {
// Devuelve undefined si no hay más páginas
return lastPage.hasMore ? pages.length + 1 : undefined;
},
});
if (isLoading) return <div>Cargando...</div>;
return (
<div>
{data.pages.map((page, i) => (
<div key={i}>
{page.posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? 'Cargando más...'
: hasNextPage
? 'Cargar Más'
: 'No hay más posts'}
</button>
</div>
);
}
Estados de Consulta
| Estado | Descripción |
|---|---|
isPending |
La consulta aún no tiene datos (primera carga) |
isLoading |
isPending && isFetching (carga inicial) |
isFetching |
Cualquier obtención en progreso (incluyendo segundo plano) |
isStale |
Los datos son más antiguos que staleTime |
isSuccess |
La consulta tiene datos y sin error |
💡 Mejores Prácticas
- • Usa claves de consulta descriptivas: ['todos', { filter, sort }]
- • Establece staleTime apropiado para tus necesidades de frescura de datos
- • Usa la opción enabled para controlar cuándo se ejecutan las consultas
- • Invalida consultas después de mutaciones
- • Usa actualizaciones optimistas para mejor UX
- • Instala React Query DevTools para depuración