DevToolBoxFREE
BlogAdvertise

React Query Patterns 2026:TanStack QueryでのData Fetching・キャッシュ・Mutations

13分by DevToolBox

TanStack QueryはReactアプリでのサーバー状態管理の標準ソリューションです。

useQuery:データの取得とキャッシュ

useQueryはqueryKeyをキャッシュキーとしてサーバー状態をサブスクライブします。

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:作成・更新・削除

useMutationはonSuccessコールバックを使ってCRUD操作を処理します。

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>
    );
}

即時UIのための楽観的更新

楽観的更新はサーバーの応答前に変更をUIに適用します。

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'] });
        },
    });
}

ページネーションのための無限クエリ

useInfiniteQueryはカーソルベースのページネーションを処理します。

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>
    );
}

クエリキーパターンとプリフェッチ

一貫したキーファクトリがキャッシュの不整合を防ぎます。

// 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 その他のソリューション

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

ベストプラクティス

  • クエリキーファクトリを一貫性のために使用。
  • データの新鮮さに応じてstaleTimeを設定。
  • 常にローディングとエラー状態を提供。
  • クイック操作に楽観的更新を使用。
  • ホバー時やルート遷移時にデータをプリフェッチ。

よくある質問

staleTimeとgcTimeの違いは?

staleTimeはデータが古くなるタイミング、gcTimeはメモリに保持する期間を制御します。

TanStack Queryは競合状態をどう処理する?

同じリクエストを自動で重複排除します。

ReduxやZustandと併用すべき?

補完的:TanStack Queryはサーバー状態、Zustandはクライアント状態に。

認証エラーをグローバルに処理するには?

QueryClientのグローバルonErrorコールバックを使用。

TanStack Query v5の変更点は?

cacheTimeをgcTimeに改名、queryFnパラメータをオブジェクト化、RSCサポート追加。

𝕏 Twitterin LinkedIn
この記事は役に立ちましたか?

Stay Updated

Get weekly dev tips and new tool announcements.

No spam. Unsubscribe anytime.

Partner Picks

Sponsor this article

Place your product next to this developer topic with tracked clicks.

Ask about article sponsorship

This site uses cookies for analytics and to display ads. By continuing to browse, you agree. Privacy Policy