本文へ移動
cccskills
無料GitHub で公開

tanstack-query-setup

Implements TanStack Query (React Query) for server state management with caching, mutations, optimistic updates, and infinite queries. Use when users request "react query", "tanstack query", "data fetching", "cache management", or "server state".

インストール方法を見る

含まれるファイル(1)

  • SKILL.md15.7 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

TanStack Query Setup

Manage server state with powerful caching, background updates, and optimistic UI.

Core Workflow

  1. Install and configure: Set up QueryClient
  2. Create queries: Define data fetching hooks
  3. Add mutations: Handle data modifications
  4. Enable caching: Configure stale times
  5. Implement optimistic updates: Instant UI feedback
  6. Add infinite queries: Pagination and infinite scroll

Installation

npm install @tanstack/react-query @tanstack/react-query-devtools

Provider Setup

Next.js App Router

// app/providers.tsx
'use client';

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

export function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: {
            staleTime: 60 * 1000, // 1 minute
            gcTime: 5 * 60 * 1000, // 5 minutes (formerly cacheTime)
            retry: 1,
            refetchOnWindowFocus: false,
          },
        },
      })
  );

  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}
// app/layout.tsx
import { Providers } from './providers';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

Basic Queries

Simple Query

// hooks/useUsers.ts
import { useQuery } from '@tanstack/react-query';

interface User {
  id: string;
  name: string;
  email: string;
}

async function fetchUsers(): Promise<User[]> {
  const response = await fetch('/api/users');
  if (!response.ok) {
    throw new Error('Failed to fetch users');
  }
  return response.json();
}

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

// Usage
function UsersList() {
  const { data: users, isLoading, error } = useUsers();

  if (isLoading) return <Spinner />;
  if (error) return <Error message={error.message} />;

  return (
    <ul>
      {users?.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Query with Parameters

// hooks/useUser.ts
import { useQuery } from '@tanstack/react-query';

async function fetchUser(userId: string): Promise<User> {
  const response = await fetch(`/api/users/${userId}`);
  if (!response.ok) {
    throw new Error('Failed to fetch user');
  }
  return response.json();
}

export function useUser(userId: string) {
  return useQuery({
    queryKey: ['users', userId],
    queryFn: () => fetchUser(userId),
    enabled: !!userId, // Only fetch when userId exists
  });
}

// Usage
function UserProfile({ userId }: { userId: string }) {
  const { data: user, isLoading } = useUser(userId);

  if (isLoading) return <Skeleton />;

  return <div>{user?.name}</div>;
}

Query with Filters

// hooks/useProducts.ts
interface ProductFilters {
  category?: string;
  minPrice?: number;
  maxPrice?: number;
  search?: string;
}

async function fetchProducts(filters: ProductFilters): Promise<Product[]> {
  const params = new URLSearchParams();
  if (filters.category) params.set('category', filters.category);
  if (filters.minPrice) params.set('minPrice', String(filters.minPrice));
  if (filters.maxPrice) params.set('maxPrice', String(filters.maxPrice));
  if (filters.search) params.set('search', filters.search);

  const response = await fetch(`/api/products?${params}`);
  return response.json();
}

export function useProducts(filters: ProductFilters) {
  return useQuery({
    queryKey: ['products', filters],
    queryFn: () => fetchProducts(filters),
    placeholderData: (previousData) => previousData, // Keep previous data while fetching
  });
}

Mutations

Basic Mutation

// hooks/useCreateUser.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';

interface CreateUserDto {
  name: string;
  email: string;
}

async function createUser(data: CreateUserDto): Promise<User> {
  const response = await fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  });

  if (!response.ok) {
    throw new Error('Failed to create user');
  }

  return response.json();
}

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

  return useMutation({
    mutationFn: createUser,
    onSuccess: (newUser) => {
      // Invalidate and refetch users list
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
    onError: (error) => {
      console.error('Failed to create user:', error);
    },
  });
}

// Usage
function CreateUserForm() {
  const { mutate, isPending, isError, error } = useCreateUser();

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);

    mutate({
      name: formData.get('name') as string,
      email: formData.get('email') as string,
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" required />
      <input name="email" type="email" required />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Creating...' : 'Create User'}
      </button>
      {isError && <p className="text-red-500">{error.message}</p>}
    </form>
  );
}

Update and Delete

// hooks/useUpdateUser.ts
export function useUpdateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async ({ id, data }: { id: string; data: Partial<User> }) => {
      const response = await fetch(`/api/users/${id}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data),
      });
      return response.json();
    },
    onSuccess: (updatedUser) => {
      // Update the single user cache
      queryClient.setQueryData(['users', updatedUser.id], updatedUser);
      // Invalidate the list
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });
}

// hooks/useDeleteUser.ts
export function useDeleteUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (userId: string) => {
      await fetch(`/api/users/${userId}`, { method: 'DELETE' });
      return userId;
    },
    onSuccess: (deletedId) => {
      // Remove from cache
      queryClient.removeQueries({ queryKey: ['users', deletedId] });
      // Invalidate list
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });
}

Optimistic Updates

List Update

// hooks/useToggleTodo.ts
export function useToggleTodo() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async ({ id, completed }: { id: string; completed: boolean }) => {
      const response = await fetch(`/api/todos/${id}`, {
        method: 'PATCH',
        body: JSON.stringify({ completed }),
      });
      return response.json();
    },

    // Optimistic update
    onMutate: async ({ id, completed }) => {
      // Cancel outgoing refetches
      await queryClient.cancelQueries({ queryKey: ['todos'] });

      // Snapshot previous value
      const previousTodos = queryClient.getQueryData<Todo[]>(['todos']);

      // Optimistically update
      queryClient.setQueryData<Todo[]>(['todos'], (old) =>
        old?.map((todo) =>
          todo.id === id ? { ...todo, completed } : todo
        )
      );

      // Return context for rollback
      return { previousTodos };
    },

    // Rollback on error
    onError: (err, variables, context) => {
      if (context?.previousTodos) {
        queryClient.setQueryData(['todos'], context.previousTodos);
      }
    },

    // Refetch after success or error
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    },
  });
}

Create with Optimistic Add

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

  return useMutation({
    mutationFn: async (text: string) => {
      const response = await fetch('/api/todos', {
        method: 'POST',
        body: JSON.stringify({ text }),
      });
      return response.json();
    },

    onMutate: async (text) => {
      await queryClient.cancelQueries({ queryKey: ['todos'] });

      const previousTodos = queryClient.getQueryData<Todo[]>(['todos']);

      // Add optimistic todo with temp id
      const optimisticTodo: Todo = {
        id: `temp-${Date.now()}`,
        text,
        completed: false,
      };

      queryClient.setQueryData<Todo[]>(['todos'], (old) => [
        ...(old || []),
        optimisticTodo,
      ]);

      return { previousTodos, optimisticTodo };
    },

    onError: (err, text, context) => {
      if (context?.previousTodos) {
        queryClient.setQueryData(['todos'], context.previousTodos);
      }
    },

    onSuccess: (newTodo, text, context) => {
      // Replace optimistic todo with real one
      queryClient.setQueryData<Todo[]>(['todos'], (old) =>
        old?.map((todo) =>
          todo.id === context?.optimisticTodo.id ? newTodo : todo
        )
      );
    },
  });
}

Infinite Queries

Cursor-Based Pagination

// hooks/useInfinitePosts.ts
import { useInfiniteQuery } from '@tanstack/react-query';

interface PostsPage {
  posts: Post[];
  nextCursor?: string;
}

async function fetchPosts({ pageParam }: { pageParam?: string }): Promise<PostsPage> {
  const url = pageParam
    ? `/api/posts?cursor=${pageParam}`
    : '/api/posts';

  const response = await fetch(url);
  return response.json();
}

export function useInfinitePosts() {
  return useInfiniteQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    initialPageParam: undefined,
    getNextPageParam: (lastPage) => lastPage.nextCursor,
  });
}

// Usage with intersection observer
function PostsFeed() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfinitePosts();

  const loadMoreRef = useRef<HTMLDivElement>(null);

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

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

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

  return (
    <div>
      {data?.pages.map((page, pageIndex) => (
        <Fragment key={pageIndex}>
          {page.posts.map((post) => (
            <PostCard key={post.id} post={post} />
          ))}
        </Fragment>
      ))}

      <div ref={loadMoreRef} className="h-10">
        {isFetchingNextPage && <Spinner />}
      </div>
    </div>
  );
}

Offset-Based Pagination

export function useInfiniteProducts() {
  return useInfiniteQuery({
    queryKey: ['products'],
    queryFn: async ({ pageParam = 0 }) => {
      const response = await fetch(`/api/products?offset=${pageParam}&limit=20`);
      return response.json();
    },
    initialPageParam: 0,
    getNextPageParam: (lastPage, allPages) => {
      // Stop if less than limit returned
      if (lastPage.products.length < 20) return undefined;
      return allPages.length * 20;
    },
  });
}

Query Factories

Organized Query Keys

// lib/queries/users.ts
import { queryOptions } from '@tanstack/react-query';

export const userQueries = {
  all: () => queryOptions({
    queryKey: ['users'],
    queryFn: fetchUsers,
  }),

  detail: (id: string) => queryOptions({
    queryKey: ['users', id],
    queryFn: () => fetchUser(id),
  }),

  list: (filters: UserFilters) => queryOptions({
    queryKey: ['users', 'list', filters],
    queryFn: () => fetchUsers(filters),
  }),

  posts: (userId: string) => queryOptions({
    queryKey: ['users', userId, 'posts'],
    queryFn: () => fetchUserPosts(userId),
  }),
};

// Usage
function UserProfile({ userId }: { userId: string }) {
  const userQuery = useQuery(userQueries.detail(userId));
  const postsQuery = useQuery(userQueries.posts(userId));
  // ...
}

// Invalidation
queryClient.invalidateQueries({ queryKey: ['users'] }); // All user queries
queryClient.invalidateQueries({ queryKey: ['users', userId] }); // Specific user

Prefetching

On Hover

function UserLink({ userId, children }: { userId: string; children: React.ReactNode }) {
  const queryClient = useQueryClient();

  const prefetch = () => {
    queryClient.prefetchQuery(userQueries.detail(userId));
  };

  return (
    <Link
      href={`/users/${userId}`}
      onMouseEnter={prefetch}
      onFocus={prefetch}
    >
      {children}
    </Link>
  );
}

In Server Components

// app/users/page.tsx
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
import { userQueries } from '@/lib/queries/users';
import { UsersList } from './UsersList';

export default async function UsersPage() {
  const queryClient = new QueryClient();

  await queryClient.prefetchQuery(userQueries.all());

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <UsersList />
    </HydrationBoundary>
  );
}

Dependent Queries

function UserPosts({ userId }: { userId: string }) {
  // First query
  const userQuery = useQuery({
    queryKey: ['users', userId],
    queryFn: () => fetchUser(userId),
  });

  // Dependent query - only runs when user is loaded
  const postsQuery = useQuery({
    queryKey: ['users', userId, 'posts'],
    queryFn: () => fetchUserPosts(userId),
    enabled: !!userQuery.data, // Wait for user
  });

  if (userQuery.isLoading) return <Spinner />;

  return (
    <div>
      <h2>{userQuery.data?.name}'s Posts</h2>
      {postsQuery.isLoading ? (
        <Spinner />
      ) : (
        <PostsList posts={postsQuery.data} />
      )}
    </div>
  );
}

Parallel Queries

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

function Dashboard({ userIds }: { userIds: string[] }) {
  const userQueries = useQueries({
    queries: userIds.map((id) => ({
      queryKey: ['users', id],
      queryFn: () => fetchUser(id),
    })),
  });

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

  if (isLoading) return <Spinner />;

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

Best Practices

  1. Use query factories: Organized, reusable query options
  2. Set appropriate stale times: Balance freshness vs performance
  3. Optimistic updates: Instant UI feedback
  4. Prefetch on hover: Anticipate user navigation
  5. Use placeholderData: Show stale data while fetching
  6. Handle errors gracefully: Error boundaries and retry
  7. SSR with HydrationBoundary: Hydrate queries from server
  8. Separate queries and mutations: Clear data flow

Output Checklist

Every TanStack Query implementation should include:

  • QueryClient with default options
  • Provider with devtools
  • Query factories for organization
  • Proper query keys structure
  • Mutations with invalidation
  • Optimistic updates for UX
  • Loading and error states
  • Prefetching strategy
  • SSR hydration (if using Next.js)
  • Infinite queries for pagination

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Audits and implements web accessibility (a11y) following WCAG 2.1 guidelines with ARIA patterns, keyboard navigation, screen reader support, and contrast checking. Use when users request "accessibility audit", "a11y review", "WCAG compliance", "screen reader support", or "keyboard navigation".

日本語の概要は準備中です。原文の説明を表示しています。

sathishssj3/Stereix-Engine22026年10月4日 更新

Creates Architecture Decision Records documenting key technical decisions with context, alternatives considered, tradeoffs, consequences, and decision owners. Use when documenting "architecture decisions", "technical choices", "design decisions", or "ADRs".

日本語の概要は準備中です。原文の説明を表示しています。

sathishssj3/Stereix-Engine22026年10月4日 更新

Designs multi-step agent workflows with tool usage, retry logic, state management, and budget controls. Provides orchestration diagrams, tool execution order, fallback strategies, and cost limits. Use for "AI agents", "agentic workflows", "multi-step AI", or "autonomous systems".

日本語の概要は準備中です。原文の説明を表示しています。

sathishssj3/Stereix-Engine22026年10月4日 更新

MUST USE when user wants to 调研/research/搜索/search/查/找/look up anything on the internet — e.g. 全网调研 X / 帮我调研一下 X / 查一下 X / 搜搜 X / 看看大家怎么评价 X / X 上有什么讨论 / research this topic。 Also MUST USE when user mentions any platform or shares any URL/链接: 小红书/xiaohongshu/xhs, Twitter/推特/X, B站/bilibili, Reddit, Facebook, Instagram, V2EX, LinkedIn/领英/招聘/求职/jobs, YouTube, GitHub code search, 小宇宙播客, 雪球/股票行情, RSS feeds, or any web URL. 15 platforms, multi-backend routing (OpenCLI / per-platform CLIs / APIs). Zero config for 6 channels. Run `agent-reach doctor --json` to see which backend serves each platform right now. NOT for: 写报告/数据分析/翻译等内容加工(本 skill 只负责从互联网获取内容); 发帖/评论/点赞等写操作;已有专门 skill 的平台(先用专门 skill)。 【路由方式】SKILL.md 包含路由表和常用命令,复杂场景需按需阅读对应分类的 references/*.md。 分类:search / social (小红书/推特/B站/V2EX/Reddit/Facebook/Instagram) / career(LinkedIn) / dev(github) / web(网页/文章/RSS) / video(YouTube/B站/播客) / finance(雪球/股票)。

日本語の概要は準備中です。原文の説明を表示しています。

sathishssj3/Stereix-Engine22026年10月4日 更新

Orchestrates multi-agent AI systems with task delegation, agent communication, shared memory, and workflow coordination. Use when users request "multi-agent system", "agent orchestration", "AI agents", "agent coordination", or "autonomous agents".

日本語の概要は準備中です。原文の説明を表示しています。

sathishssj3/Stereix-Engine22026年10月4日 更新

Creates SLO-based alerts and operational dashboards with key charts, alert thresholds, and runbook links. Use for "alerting", "dashboards", "SLO", or "monitoring".

日本語の概要は準備中です。原文の説明を表示しています。

sathishssj3/Stereix-Engine22026年10月4日 更新

sathishssj3 のスキルをすべて見る

このスキルの問題を報告する