DevToolBoxGRATIS
Blog

Patrones React Query 2026: Fetching, Cache y Mutations con TanStack Query

13 minpor DevToolBox

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

FeatureTanStack QueryRedux ToolkitSWRApollo
CachingBuilt-in (staleTime, gcTime)ManualBuilt-inBuilt-in (Apollo-only)
Optimistic updatesFirst-classManualBasicFirst-class
Infinite queryuseInfiniteQueryManualuseSWRInfinitefetchMore
MutationsuseMutationRTK QueryNo (use fetch)useMutation
DevToolsExcellentExcellentBasicGood
REST + GraphQLBothBothREST focusedGraphQL 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.

Herramientas relacionadas

𝕏 Twitterin LinkedIn
¿Fue útil?

Mantente actualizado

Recibe consejos de desarrollo y nuevas herramientas.

Sin spam. Cancela cuando quieras.

Prueba estas herramientas relacionadas

{ }JSON FormatterB→Base64 Encode OnlineIDUUID Generator Online

Artículos relacionados

React Server Components: Guía Completa 2026

Domina React Server Components: arquitectura, fetching de datos, streaming y migración.

TypeScript Type Guards: Guía Completa de Verificación de Tipos

Domina type guards en TypeScript: typeof, instanceof, in y guards personalizados.

Next.js App Router: Guia de migracion completa 2026

Domina el Next.js App Router con esta guia completa. Server Components, obtencion de datos, layouts, streaming, Server Actions y migracion paso a paso desde Pages Router.