Manual Completo de React Query v4+

Guía definitiva para dominar la gestión de datos asíncronos en React con la librería más popular.

Fundamentos
Intermedio
Avanzado
Integraciones

1. Fundamentos de React Query

1.1 ¿Qué es React Query?

React Query es una librería para:

1.2 Instalación

Terminal
npm install @tanstack/react-query
# Opcional: Devtools
npm install @tanstack/react-query-devtools

1.3 Configuración Básica

src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from './App';

// Crear cliente de React Query
const queryClient = new QueryClient();

ReactDOM.render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>,
  document.getElementById('root')
);

1.4 Primer Query

src/components/Posts.jsx
import { useQuery } from '@tanstack/react-query';

const fetchPosts = async () => {
  const response = await fetch('https://jsonplaceholder.typicode.com/posts');
  return response.json();
};

function Posts() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
  });

  if (isLoading) return <div>Cargando...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <ul>
      {data.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

export default Posts;

1.5 Estados de un Query

Estado Descripción Propiedades Relacionadas
Loading Primera carga de datos isLoading, isFetching
Error Falló la petición error, isError
Success Datos cargados correctamente data, isSuccess
Idle Query deshabilitado enabled: false

💡 Buenas Prácticas Iniciales

2. React Query Intermedio

2.1 Mutaciones (Actualización de Datos)

src/components/AddPost.jsx
import { useMutation, useQueryClient } from '@tanstack/react-query';

const addPost = async (newPost) => {
  const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
    method: 'POST',
    body: JSON.stringify(newPost),
  });
  return response.json();
};

function AddPost() {
  const queryClient = useQueryClient();
  const mutation = useMutation({
    mutationFn: addPost,
    onSuccess: () => {
      // Invalidar cache y refetch
      queryClient.invalidateQueries({ queryKey: ['posts'] });
    },
  });

  const handleSubmit = (e) => {
    e.preventDefault();
    const formData = new FormData(e.target);
    const newPost = {
      title: formData.get('title'),
      body: formData.get('body'),
    };
    mutation.mutate(newPost);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="title" placeholder="Título" required />
      <textarea name="body" placeholder="Contenido" required />
      <button type="submit" disabled={mutation.isLoading}>
        {mutation.isLoading ? 'Enviando...' : 'Crear Post'}
      </button>
    </form>
  );
}

2.2 Paginación

src/components/PaginatedPosts.jsx
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';

const fetchPosts = async ({ pageParam = 1 }) => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/posts?_page=${pageParam}&_limit=10`
  );
  return response.json();
};

function PaginatedPosts() {
  const [page, setPage] = useState(1);
  
  const { data, isLoading, isError } = useQuery({
    queryKey: ['posts', page],
    queryFn: () => fetchPosts({ pageParam: page }),
    keepPreviousData: true,
  });

  return (
    <div>
      {isLoading ? (
        <div>Cargando...</div>
      ) : isError ? (
        <div>Error al cargar posts</div>
      ) : (
        <>
          <ul>
            {data.map(post => (
              <li key={post.id}>{post.title}</li>
            ))}
          </ul>
          <button 
            onClick={() => setPage(old => Math.max(old - 1, 1))}
            disabled={page === 1}
          >
            Anterior
          </button>
          <span>Página {page}</span>
          <button
            onClick={() => setPage(old => old + 1)}
            disabled={data.length < 10}
          >
            Siguiente
          </button>
        </>
      )}
    </div>
  );
}

2.3 Infinite Scroll

src/components/InfinitePosts.jsx
import { useInfiniteQuery } from '@tanstack/react-query';

const fetchPosts = async ({ pageParam = 1 }) => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/posts?_page=${pageParam}&_limit=10`
  );
  return response.json();
};

function InfinitePosts() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    status,
  } = useInfiniteQuery({
    queryKey: ['infinitePosts'],
    queryFn: fetchPosts,
    getNextPageParam: (lastPage, allPages) => {
      return lastPage.length ? allPages.length + 1 : undefined;
    },
  });

  return status === 'loading' ? (
    <p>Cargando...</p>
  ) : status === 'error' ? (
    <p>Error al cargar datos</p>
  ) : (
    <>
      <ul>
        {data.pages.map((page, i) => (
          <React.Fragment key={i}>
            {page.map(post => (
              <li key={post.id}>{post.title}</li>
            ))}
          </React.Fragment>
        ))}
      </ul>
      <button
        onClick={() => fetchNextPage()}
        disabled={!hasNextPage || isFetchingNextPage}
      >
        {isFetchingNextPage
          ? 'Cargando más...'
          : hasNextPage
          ? 'Cargar más'
          : 'No hay más datos'}
      </button>
    </>
  );
}

2.4 Prefetching

src/components/PostList.jsx
import { useQueryClient } from '@tanstack/react-query';

function PostList() {
  const queryClient = useQueryClient();

  const prefetchPost = async (postId) => {
    await queryClient.prefetchQuery({
      queryKey: ['post', postId],
      queryFn: () => fetchPost(postId),
    });
  };

  return (
    <ul>
      {posts.map(post => (
        <li 
          key={post.id}
          onMouseEnter={() => prefetchPost(post.id)}
        >
          {post.title}
        </li>
      ))}
    </ul>
  );
}

3. React Query Avanzado

3.1 Optimistic Updates

src/components/OptimisticTodo.jsx
import { useMutation, useQueryClient } from '@tanstack/react-query';

function OptimisticTodo() {
  const queryClient = useQueryClient();
  
  const mutation = useMutation({
    mutationFn: updateTodo,
    onMutate: async (newTodo) => {
      // Cancelar queries en curso
      await queryClient.cancelQueries({ queryKey: ['todos'] });
      
      // Snapshot del valor anterior
      const previousTodos = queryClient.getQueryData(['todos']);
      
      // Actualización optimista
      queryClient.setQueryData(['todos'], (old) => 
        old.map(todo => 
          todo.id === newTodo.id ? { ...todo, ...newTodo } : todo
        )
      );
      
      // Retornar contexto con snapshot
      return { previousTodos };
    },
    onError: (err, newTodo, context) => {
      // Revertir al snapshot en caso de error
      queryClient.setQueryData(['todos'], context.previousTodos);
    },
    onSettled: () => {
      // Refetch para asegurar sincronización
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    },
  });

  const handleToggle = (todo) => {
    mutation.mutate({
      ...todo,
      completed: !todo.completed,
    });
  };

  // Renderizar lista de todos...
}

3.2 Custom Hooks

src/hooks/usePosts.js
import { useQuery } from '@tanstack/react-query';

const fetchPosts = async () => {
  const response = await fetch('https://jsonplaceholder.typicode.com/posts');
  return response.json();
};

export function usePosts() {
  return useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    staleTime: 5 * 60 * 1000, // 5 minutos
  });
}
src/components/Posts.jsx
import { usePosts } from '../hooks/usePosts';

function Posts() {
  const { data, isLoading, error } = usePosts();
  
  // Renderizar posts...
}

3.3 Configuración Global

src/main.jsx
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minutos
      retry: 2, // Reintentar 2 veces antes de mostrar error
      refetchOnWindowFocus: false,
    },
    mutations: {
      onError: (error) => {
        toast.error(error.message);
      },
    },
  },
});

3.4 Testing con React Query

src/components/__tests__/Posts.test.jsx
import { render, screen, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import Posts from '../Posts';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: false,
    },
  },
});

const wrapper = ({ children }) => (
  <QueryClientProvider client={queryClient}>
    {children}
  </QueryClientProvider>
);

test('muestra posts correctamente', async () => {
  // Mock de fetch
  global.fetch = jest.fn(() =>
    Promise.resolve({
      json: () => Promise.resolve([{ id: 1, title: 'Test Post' }]),
    })
  );

  render(<Posts />, { wrapper });

  expect(screen.getByText(/cargando/i)).toBeInTheDocument();

  await waitFor(() => {
    expect(screen.getByText('Test Post')).toBeInTheDocument();
  });
});

4. Integraciones con React Query

4.1 React Query + TypeScript

src/types/post.ts
export interface Post {
  id: number;
  title: string;
  body: string;
  userId: number;
}
src/hooks/usePosts.ts
import { useQuery } from '@tanstack/react-query';
import { Post } from '../types/post';

const fetchPosts = async (): Promise<Post[]> => {
  const response = await fetch('https://jsonplaceholder.typicode.com/posts');
  return response.json();
};

export function usePosts() {
  return useQuery<Post[], Error>({
    queryKey: ['posts'],
    queryFn: fetchPosts,
  });
}

4.2 React Query + Next.js

src/pages/_app.tsx
import type { AppProps } from 'next/app';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

const queryClient = new QueryClient();

export default function App({ Component, pageProps }: AppProps) {
  return (
    <QueryClientProvider client={queryClient}>
      <Component {...pageProps} />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

4.3 React Query + Zustand

src/store/postsStore.ts
import { create } from 'zustand';
import { useQuery } from '@tanstack/react-query';

interface PostsState {
  posts: Post[];
  setPosts: (posts: Post[]) => void;
}

const usePostsStore = create<PostsState>((set) => ({
  posts: [],
  setPosts: (posts) => set({ posts }),
}));

export const usePostsQuery = () => {
  const { setPosts } = usePostsStore();
  
  return useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    onSuccess: (data) => {
      setPosts(data);
    },
  });
};

4.4 React Query + GraphQL

Terminal
npm install graphql-request
src/hooks/useGraphQLData.ts
import { useQuery } from '@tanstack/react-query';
import { request } from 'graphql-request';

const endpoint = 'https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr';

const fetchUsers = async () => {
  const query = `
    {
      allUsers {
        id
        name
        email
      }
    }
  `;
  return request(endpoint, query);
};

export function useUsers() {
  return useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  });
}

4.5 React Query Devtools

src/main.jsx
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <MyApp />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}