NYC
skills/smithery/ai/tanstack-query

tanstack-query

SKILL.md

TanStack Query (React Query) v5

Powerful asynchronous state management for React. TanStack Query makes fetching, caching, synchronizing, and updating server state in your React applications a breeze.

When to Use This Skill

  • Fetching data from REST APIs or GraphQL endpoints
  • Managing server state and cache lifecycle
  • Implementing mutations (create, update, delete operations)
  • Building infinite scroll or load-more patterns
  • Handling optimistic UI updates
  • Synchronizing data across components
  • Implementing background data refetching
  • Managing complex async state without Redux or other state managers

Quick Start Workflow

1. Installation

npm install @tanstack/react-query
# or
pnpm add @tanstack/react-query
# or
yarn add @tanstack/react-query

2. Setup QueryClient

Wrap your application with QueryClientProvider:

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
    </QueryClientProvider>
  );
}

3. Basic Query

import { useQuery } from '@tanstack/react-query';

function TodoList() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['todos'],
    queryFn: async () => {
      const res = await fetch('https://api.example.com/todos');
      if (!res.ok) throw new Error('Network response was not ok');
      return res.json();
    },
  });

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

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

4. Basic Mutation

import { useMutation, useQueryClient } from '@tanstack/react-query';

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

  const mutation = useMutation({
    mutationFn: async (newTodo) => {
      const res = await fetch('https://api.example.com/todos', {
        method: 'POST',
        body: JSON.stringify(newTodo),
        headers: { 'Content-Type': 'application/json' },
      });
      return res.json();
    },
    onSuccess: () => {
      // Invalidate and refetch todos
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    },
  });

  return (
    <button onClick={() => mutation.mutate({ title: 'New Todo' })}>
      {mutation.isPending ? 'Creating...' : 'Create Todo'}
    </button>
  );
}

Core Concepts

Query Keys

Query keys uniquely identify queries and are used for caching. They must be arrays.

// Simple key
useQuery({ queryKey: ['todos'], queryFn: fetchTodos });

// Key with variables
useQuery({ queryKey: ['todo', todoId], queryFn: () => fetchTodo(todoId) });

// Hierarchical keys
useQuery({ queryKey: ['todos', 'list', { filters, page }], queryFn: fetchTodos });

Query key matching:

  • ['todos'] - exact match
  • ['todos', { page: 1 }] - exact match with object
  • { queryKey: ['todos'] } - matches all queries starting with 'todos'

Query Functions

Query functions must return a promise that resolves data or throws an error:

// Using fetch
queryFn: async () => {
  const res = await fetch(url);
  if (!res.ok) throw new Error('Failed to fetch');
  return res.json();
}

// Using axios
queryFn: () => axios.get(url).then(res => res.data)

// With query key access
queryFn: ({ queryKey }) => {
  const [_, todoId] = queryKey;
  return fetchTodo(todoId);
}

Important Defaults

Understanding defaults is crucial for optimal usage:

  • staleTime: 0 - Queries become stale immediately by default
  • gcTime: 5 minutes - Unused/inactive cache data remains in memory for 5 minutes
  • retry: 3 - Failed queries retry 3 times with exponential backoff
  • refetchOnWindowFocus: true - Queries refetch when window regains focus
  • refetchOnReconnect: true - Queries refetch when network reconnects
// Override defaults globally
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5 minutes
      gcTime: 1000 * 60 * 10, // 10 minutes
    },
  },
});

// Or per query
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: 1000 * 60, // 1 minute
  retry: 5,
});

Query Status and Fetch Status

Queries have two important states:

Query Status:

  • pending - No cached data, query is executing
  • error - Query encountered an error
  • success - Query succeeded and data is available

Fetch Status:

  • fetching - Query function is executing
  • paused - Query wants to fetch but is paused (offline)
  • idle - Query is not fetching
const { data, status, fetchStatus, isLoading, isFetching } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
});

// isLoading = status === 'pending'
// isFetching = fetchStatus === 'fetching'

Query Invalidation

Mark queries as stale to trigger refetches:

const queryClient = useQueryClient();

// Invalidate all todos queries
queryClient.invalidateQueries({ queryKey: ['todos'] });

// Invalidate specific query
queryClient.invalidateQueries({ queryKey: ['todo', todoId] });

// Invalidate and refetch immediately
queryClient.invalidateQueries({
  queryKey: ['todos'],
  refetchType: 'active' // only refetch active queries
});

Mutations

Mutations are used for creating, updating, or deleting data:

const mutation = useMutation({
  mutationFn: (newTodo) => {
    return fetch('/api/todos', {
      method: 'POST',
      body: JSON.stringify(newTodo),
    });
  },
  onSuccess: (data, variables, context) => {
    console.log('Success!', data);
  },
  onError: (error, variables, context) => {
    console.error('Error:', error);
  },
  onSettled: (data, error, variables, context) => {
    console.log('Mutation finished');
  },
});

// Trigger mutation
mutation.mutate({ title: 'New Todo' });

// With async/await
mutation.mutateAsync({ title: 'New Todo' })
  .then(data => console.log(data))
  .catch(error => console.error(error));

React Suspense Integration

TanStack Query supports React Suspense with dedicated hooks:

import { useSuspenseQuery } from '@tanstack/react-query';

function TodoList() {
  // This will suspend the component until data is ready
  const { data } = useSuspenseQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  });

  // No need for loading states - handled by Suspense boundary
  return (
    <ul>
      {data.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}

// In parent component
function App() {
  return (
    <Suspense fallback={<div>Loading todos...</div>}>
      <TodoList />
    </Suspense>
  );
}

Advanced Topics

For detailed information on advanced patterns, see the reference files:

Infinite Queries

For implementing infinite scroll and load-more patterns:

  • See references/infinite-queries.md for comprehensive guide
  • Covers useInfiniteQuery hook
  • Bidirectional pagination
  • getNextPageParam and getPreviousPageParam
  • Refetching and background updates

Optimistic Updates

For updating UI before server confirmation:

  • See references/optimistic-updates.md for detailed patterns
  • Optimistic mutations
  • Rollback on error
  • Context for cancellation
  • UI feedback strategies

TypeScript Support

For full type safety and inference:

  • See references/typescript.md for complete TypeScript guide
  • Type inference from query functions
  • Generic type parameters
  • Typing query options
  • Custom hooks with types
  • Error type narrowing

Query Invalidation Patterns

For advanced cache invalidation strategies:

  • See references/query-invalidation.md
  • Partial matching
  • Predicate functions
  • Refetch strategies
  • Query filters

Performance Optimization

For optimizing query performance:

  • See references/performance.md
  • Query deduplication
  • Structural sharing
  • Memory management
  • Query splitting strategies

DevTools

TanStack Query DevTools provide visual insights into query state:

npm install @tanstack/react-query-devtools
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

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

DevTools features:

  • View all queries and their states
  • Inspect query data and errors
  • Manually trigger refetches
  • Invalidate queries
  • Monitor cache lifecycle

Common Patterns

Dependent Queries

Run queries in sequence when one depends on another:

// First query
const { data: user } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
});

// Second query depends on first
const { data: projects } = useQuery({
  queryKey: ['projects', user?.id],
  queryFn: () => fetchProjects(user.id),
  enabled: !!user?.id, // Only run when user.id is available
});

Parallel Queries

Multiple independent queries in one component:

function Dashboard() {
  const users = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
  const posts = useQuery({ queryKey: ['posts'], queryFn: fetchPosts });
  const stats = useQuery({ queryKey: ['stats'], queryFn: fetchStats });

  if (users.isLoading || posts.isLoading || stats.isLoading) {
    return <div>Loading...</div>;
  }

  // All queries succeeded
  return <DashboardView users={users.data} posts={posts.data} stats={stats.data} />;
}

Dynamic Parallel Queries

Use useQueries for dynamic number of queries:

import { useQueries } from '@tanstack/react-query';

function TodoLists({ listIds }) {
  const results = useQueries({
    queries: listIds.map((id) => ({
      queryKey: ['list', id],
      queryFn: () => fetchList(id),
    })),
  });

  const isLoading = results.some(result => result.isLoading);
  const data = results.map(result => result.data);

  return <Lists data={data} />;
}

Prefetching

Prefetch data before it's needed:

const queryClient = useQueryClient();

// Prefetch on hover
function TodoListLink({ id }) {
  const prefetch = () => {
    queryClient.prefetchQuery({
      queryKey: ['todo', id],
      queryFn: () => fetchTodo(id),
      staleTime: 1000 * 60 * 5, // Cache for 5 minutes
    });
  };

  return (
    <Link to={`/todo/${id}`} onMouseEnter={prefetch}>
      View Todo
    </Link>
  );
}

Initial Data

Provide initial data to avoid loading states:

function TodoDetail({ todoId, initialTodo }) {
  const { data } = useQuery({
    queryKey: ['todo', todoId],
    queryFn: () => fetchTodo(todoId),
    initialData: initialTodo, // Use this data immediately
    staleTime: 1000 * 60, // Consider fresh for 1 minute
  });

  return <div>{data.title}</div>;
}

Placeholder Data

Show placeholder while loading:

const { data, isPlaceholderData } = useQuery({
  queryKey: ['todos', page],
  queryFn: () => fetchTodos(page),
  placeholderData: (previousData) => previousData, // Keep previous data while loading
});

// Or use static placeholder
const { data } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  placeholderData: { items: [], total: 0 },
});

Error Handling

Query Errors

const { error, isError } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  retry: 3,
  retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
});

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

Global Error Handling

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      onError: (error) => {
        console.error('Query error:', error);
        // Show toast notification, etc.
      },
    },
    mutations: {
      onError: (error) => {
        console.error('Mutation error:', error);
      },
    },
  },
});

Error Boundaries

Combine with React Error Boundaries:

import { useQuery } from '@tanstack/react-query';
import { ErrorBoundary } from 'react-error-boundary';

function TodoList() {
  const { data } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    throwOnError: true, // Throw errors to error boundary
  });

  return <div>{/* render data */}</div>;
}

function App() {
  return (
    <ErrorBoundary fallback={<div>Something went wrong</div>}>
      <TodoList />
    </ErrorBoundary>
  );
}

Best Practices

  1. Use Query Keys Wisely

    • Structure keys hierarchically: ['todos', 'list', { filters }]
    • Include all variables in the key
    • Keep keys consistent across your app
  2. Set Appropriate staleTime

    • Static data: staleTime: Infinity
    • Frequently changing: staleTime: 0 (default)
    • Moderately changing: staleTime: 1000 * 60 * 5 (5 minutes)
  3. Handle Loading and Error States

    • Always check isLoading and error
    • Provide meaningful loading indicators
    • Show user-friendly error messages
  4. Optimize Refetching

    • Disable unnecessary refetches with refetchOnWindowFocus: false
    • Use staleTime to reduce refetches
    • Consider using refetchInterval for polling
  5. Invalidate Efficiently

    • Invalidate specific queries, not all queries
    • Use query key prefixes for related queries
    • Combine with optimistic updates for better UX
  6. Use TypeScript

    • Type your query functions for type inference
    • Use generic type parameters when needed
    • Enable strict type checking
  7. Leverage DevTools

    • Install DevTools in development
    • Monitor query behavior
    • Debug cache issues

Resources

Migration from v4

If you're upgrading from React Query v4:

Weekly Installs
1
Repository
smithery/ai
First Seen
8 days ago
Installed on
cline1