Claude Code Plugins

Community-maintained marketplace

Feedback

query-patterns

@gizix/cc_projects
0
0

Implement TanStack Query (React Query) patterns for data fetching, caching, mutations, and optimistic updates. Use when setting up API integration or data fetching strategies.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name query-patterns
description Implement TanStack Query (React Query) patterns for data fetching, caching, mutations, and optimistic updates. Use when setting up API integration or data fetching strategies.
allowed-tools Read, Write, Grep

You are a TanStack Query (React Query) patterns expert. You help implement efficient, scalable data fetching patterns with proper TypeScript types and caching strategies.

React Query Pattern Implementations

1. Organized Query Structure

Separate concerns: API calls, query keys, and query hooks.

// services/api/users.ts - API Layer
import axios from 'axios';

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Add auth token interceptor
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

export const usersApi = {
  getAll: async (params?: GetUsersParams) => {
    const { data } = await api.get<User[]>('/users', { params });
    return data;
  },

  getById: async (id: string) => {
    const { data } = await api.get<User>(`/users/${id}`);
    return data;
  },

  create: async (user: CreateUserInput) => {
    const { data } = await api.post<User>('/users', user);
    return data;
  },

  update: async (id: string, updates: Partial<User>) => {
    const { data} = await api.patch<User>(`/users/${id}`, updates);
    return data;
  },

  delete: async (id: string) => {
    await api.delete(`/users/${id}`);
  },
};

// services/queries/queryKeys.ts - Query Key Factory
export const userKeys = {
  all: ['users'] as const,
  lists: () => [...userKeys.all, 'list'] as const,
  list: (params?: GetUsersParams) => [...userKeys.lists(), params] as const,
  details: () => [...userKeys.all, 'detail'] as const,
  detail: (id: string) => [...userKeys.details(), id] as const,
};

// services/queries/useUsers.ts - Query Hooks
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { usersApi } from '../api/users';
import { userKeys } from './queryKeys';

export function useUsers(params?: GetUsersParams) {
  return useQuery({
    queryKey: userKeys.list(params),
    queryFn: () => usersApi.getAll(params),
    staleTime: 5 * 60 * 1000, // Fresh for 5 minutes
  });
}

export function useUser(id: string) {
  return useQuery({
    queryKey: userKeys.detail(id),
    queryFn: () => usersApi.getById(id),
    enabled: !!id,
  });
}

export function useCreateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: usersApi.create,
    onSuccess: (newUser) => {
      // Invalidate and refetch
      queryClient.invalidateQueries({ queryKey: userKeys.lists() });

      // Or optimistically add to cache
      queryClient.setQueryData<User[]>(
        userKeys.list(),
        (old) => old ? [...old, newUser] : [newUser]
      );
    },
  });
}

export function useUpdateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: ({ id, updates }: { id: string; updates: Partial<User> }) =>
      usersApi.update(id, updates),

    // Optimistic update
    onMutate: async ({ id, updates }) => {
      // Cancel outgoing refetches
      await queryClient.cancelQueries({ queryKey: userKeys.detail(id) });

      // Snapshot previous value
      const previousUser = queryClient.getQueryData<User>(userKeys.detail(id));

      // Optimistically update
      queryClient.setQueryData<User>(userKeys.detail(id), (old) => {
        if (!old) return old;
        return { ...old, ...updates };
      });

      return { previousUser };
    },

    // Rollback on error
    onError: (err, { id }, context) => {
      if (context?.previousUser) {
        queryClient.setQueryData(userKeys.detail(id), context.previousUser);
      }
    },

    // Refetch after mutation
    onSettled: (data, error, { id }) => {
      queryClient.invalidateQueries({ queryKey: userKeys.detail(id) });
      queryClient.invalidateQueries({ queryKey: userKeys.lists() });
    },
  });
}

export function useDeleteUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: usersApi.delete,
    onSuccess: (_, deletedId) => {
      // Remove from cache
      queryClient.removeQueries({ queryKey: userKeys.detail(deletedId) });

      // Update lists
      queryClient.setQueriesData<User[]>(
        { queryKey: userKeys.lists() },
        (old) => old?.filter((user) => user.id !== deletedId)
      );
    },
  });
}

2. Infinite Query Pattern (Pagination)

// services/queries/useInfiniteUsers.ts
import { useInfiniteQuery } from '@tanstack/react-query';

interface PageResponse<T> {
  data: T[];
  nextCursor?: string;
  hasMore: boolean;
}

export function useInfiniteUsers(params?: GetUsersParams) {
  return useInfiniteQuery({
    queryKey: userKeys.list(params),
    queryFn: ({ pageParam }) =>
      usersApi.getAll({ ...params, cursor: pageParam }),
    getNextPageParam: (lastPage: PageResponse<User>) => {
      return lastPage.hasMore ? lastPage.nextCursor : undefined;
    },
    initialPageParam: undefined,
  });
}

// Component usage
function UserList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    isLoading,
    error,
  } = useInfiniteUsers();

  const users = data?.pages.flatMap((page) => page.data) ?? [];

  const observerTarget = useRef<HTMLDivElement>(null);

  // Infinite scroll
  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
          fetchNextPage();
        }
      },
      { threshold: 1.0 }
    );

    if (observerTarget.current) {
      observer.observe(observerTarget.current);
    }

    return () => observer.disconnect();
  }, [fetchNextPage, hasNextPage, isFetchingNextPage]);

  if (isLoading) return <LoadingSpinner />;
  if (error) return <ErrorMessage error={error} />;

  return (
    <div>
      {users.map((user) => (
        <UserCard key={user.id} user={user} />
      ))}
      <div ref={observerTarget} />
      {isFetchingNextPage && <LoadingSpinner />}
    </div>
  );
}

3. Dependent Queries

// Query that depends on another query
function UserPosts({ userId }: { userId: string }) {
  // First query
  const {
    data: user,
    isLoading: userLoading,
    error: userError,
  } = useUser(userId);

  // Second query depends on first
  const {
    data: posts,
    isLoading: postsLoading,
  } = useQuery({
    queryKey: ['posts', user?.id],
    queryFn: () => postsApi.getByUserId(user!.id),
    enabled: !!user, // Only run when user is loaded
  });

  if (userLoading) return <div>Loading user...</div>;
  if (userError) return <div>Error loading user</div>;
  if (!user) return <div>User not found</div>;
  if (postsLoading) return <div>Loading posts...</div>;

  return (
    <div>
      <h2>{user.name}'s Posts</h2>
      {posts?.map((post) => <PostCard key={post.id} post={post} />)}
    </div>
  );
}

4. Prefetching Pattern

// Prefetch on hover
function UserListItem({ user }: { user: User }) {
  const queryClient = useQueryClient();

  const prefetchUser = () => {
    queryClient.prefetchQuery({
      queryKey: userKeys.detail(user.id),
      queryFn: () => usersApi.getById(user.id),
      staleTime: 60 * 1000, // Fresh for 1 minute
    });
  };

  return (
    <Link
      to={`/users/${user.id}`}
      onMouseEnter={prefetchUser}
      onFocus={prefetchUser}
    >
      {user.name}
    </Link>
  );
}

// Prefetch next page
function PaginatedList({ page }: { page: number }) {
  const queryClient = useQueryClient();
  const { data } = useUsers({ page });

  useEffect(() => {
    // Prefetch next page
    if (data?.hasMore) {
      queryClient.prefetchQuery({
        queryKey: userKeys.list({ page: page + 1 }),
        queryFn: () => usersApi.getAll({ page: page + 1 }),
      });
    }
  }, [page, data, queryClient]);

  return <div>{/* Render users */}</div>;
}

5. Optimistic Updates with Rollback

// Like/Unlike with optimistic UI
export function useToggleLike(postId: string) {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (liked: boolean) => postsApi.toggleLike(postId, liked),

    onMutate: async (liked) => {
      // Cancel ongoing queries
      await queryClient.cancelQueries({ queryKey: postKeys.detail(postId) });

      // Snapshot
      const previousPost = queryClient.getQueryData<Post>(
        postKeys.detail(postId)
      );

      // Optimistic update
      queryClient.setQueryData<Post>(postKeys.detail(postId), (old) => {
        if (!old) return old;
        return {
          ...old,
          liked,
          likeCount: old.likeCount + (liked ? 1 : -1),
        };
      });

      return { previousPost };
    },

    onError: (err, variables, context) => {
      // Rollback
      if (context?.previousPost) {
        queryClient.setQueryData(
          postKeys.detail(postId),
          context.previousPost
        );
      }
      toast.error('Failed to update like');
    },

    onSettled: () => {
      // Refetch
      queryClient.invalidateQueries({ queryKey: postKeys.detail(postId) });
    },
  });
}

6. Parallel Queries

// Run multiple queries in parallel
function Dashboard() {
  const userQuery = useUser('current');
  const postsQuery = usePosts({ userId: 'current' });
  const notificationsQuery = useNotifications();

  // Check all loading states
  if (userQuery.isLoading || postsQuery.isLoading || notificationsQuery.isLoading) {
    return <LoadingSpinner />;
  }

  // Check for errors
  const error = userQuery.error || postsQuery.error || notificationsQuery.error;
  if (error) {
    return <ErrorMessage error={error} />;
  }

  return (
    <div>
      <UserProfile user={userQuery.data} />
      <PostList posts={postsQuery.data} />
      <NotificationList notifications={notificationsQuery.data} />
    </div>
  );
}

// Or use useQueries for dynamic parallel queries
function MultiUserView({ userIds }: { userIds: string[] }) {
  const userQueries = useQueries({
    queries: userIds.map((id) => ({
      queryKey: userKeys.detail(id),
      queryFn: () => usersApi.getById(id),
    })),
  });

  const isLoading = userQueries.some((q) => q.isLoading);
  const users = userQueries.map((q) => q.data).filter(Boolean);

  if (isLoading) return <div>Loading...</div>;

  return (
    <div>
      {users.map((user) => (
        <UserCard key={user.id} user={user} />
      ))}
    </div>
  );
}

7. Mutation with Multiple Cache Updates

// Update multiple cache locations
export function useUpdateProfile() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (updates: Partial<User>) =>
      usersApi.update('current', updates),

    onSuccess: (updatedUser) => {
      // Update current user query
      queryClient.setQueryData(userKeys.detail('current'), updatedUser);

      // Update any user lists that might contain this user
      queryClient.setQueriesData<User[]>(
        { queryKey: userKeys.lists() },
        (old) => {
          if (!old) return old;
          return old.map((user) =>
            user.id === updatedUser.id ? updatedUser : user
          );
        }
      );

      // Update any posts by this user to reflect name change
      queryClient.setQueriesData<Post[]>(
        { queryKey: ['posts'] },
        (old) => {
          if (!old) return old;
          return old.map((post) =>
            post.authorId === updatedUser.id
              ? { ...post, authorName: updatedUser.name }
              : post
          );
        }
      );
    },
  });
}

Best Practices

  1. Query Keys:

    • Use factory pattern for consistency
    • Include all variables affecting the query
    • Hierarchical structure for easy invalidation
  2. Caching Strategy:

    • Set appropriate staleTime (how long data is fresh)
    • Set appropriate gcTime (how long unused data is cached)
    • Use prefetching for better UX
  3. Error Handling:

    • Always handle errors in UI
    • Use error boundaries for critical failures
    • Configure retry logic appropriately
  4. Performance:

    • Use enabled option to prevent unnecessary requests
    • Leverage select to transform data
    • Implement optimistic updates for better UX
  5. Type Safety:

    • Define proper TypeScript interfaces
    • Type query responses
    • Type mutation variables

Common Patterns Summary

Pattern Use Case
Basic Query Fetching data
Mutation Creating/updating/deleting data
Infinite Query Pagination/infinite scroll
Dependent Queries Data that depends on other data
Prefetching Improve perceived performance
Optimistic Updates Instant UI feedback
Parallel Queries Fetch multiple resources simultaneously

This skill helps you implement efficient, scalable data fetching patterns with TanStack Query.