Menu
Zurück zum Blog
1 min read
Web Development

TanStack Query: Intelligentes Data Fetching für React

TanStack Query (React Query) für Server State Management. Caching, Background Refetch, Optimistic Updates und Best Practices.

TanStack QueryReact QueryData FetchingServer StateCachingReact
TanStack Query: Intelligentes Data Fetching für React

TanStack Query: Intelligentes Data Fetching für React

Meta-Description: TanStack Query (React Query) für Server State Management. Caching, Background Refetch, Optimistic Updates und Best Practices.

Keywords: TanStack Query, React Query, Data Fetching, Server State, Caching, React, API Client, SWR


Einführung

TanStack Query (ehemals React Query) löst das schwerste Problem in React: Server State Management. Statt manuelles Fetching, Loading States und Caching zu bauen, bietet es eine deklarative, caching-first Lösung.


Warum TanStack Query?

┌─────────────────────────────────────────────────────────────┐
│                 SERVER STATE CHALLENGES                     │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Ohne TanStack Query:                                       │
│  ├── Manuelles Loading/Error State                         │
│  ├── Keine automatische Cache-Invalidierung                │
│  ├── Duplizierte Requests                                  │
│  ├── Kein Background Refetch                               │
│  ├── Komplizierte Pagination                               │
│  └── Race Conditions                                       │
│                                                             │
│  Mit TanStack Query:                                        │
│  ├── Automatisches Loading/Error Handling                  │
│  ├── Intelligentes Caching                                 │
│  ├── Request Deduplication                                 │
│  ├── Stale-While-Revalidate                               │
│  ├── Eingebaute Pagination/Infinite Scroll                │
│  └── Automatische Retry-Logik                             │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Setup

// 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 QueryProvider({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 60 * 1000,     // 1 Minute
        gcTime: 5 * 60 * 1000,    // 5 Minuten (früher cacheTime)
        retry: 3,
        refetchOnWindowFocus: true,
        refetchOnReconnect: true
      }
    }
  }));

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

Basic Queries

useQuery

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

// API Function
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();
}

// Component
function UserProfile({ userId }: { userId: string }) {
  const {
    data: user,
    isLoading,
    isError,
    error,
    isFetching,      // true auch bei Background Refetch
    isStale,         // Daten sind "veraltet"
    refetch          // Manueller Refetch
  } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    enabled: !!userId,  // Nur fetchen wenn userId existiert
    staleTime: 5 * 60 * 1000,
    placeholderData: previousUser  // Zeige alte Daten während Refetch
  });

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

  return (
    <div>
      <h1>{user.name}</h1>
      {isFetching && <span>Updating...</span>}
    </div>
  );
}

Query mit abhängigen Daten

function UserPosts({ userId }: { userId: string }) {
  // Erst User laden
  const { data: user } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId)
  });

  // Dann Posts (nur wenn User da)
  const { data: posts } = useQuery({
    queryKey: ['posts', user?.id],
    queryFn: () => fetchUserPosts(user!.id),
    enabled: !!user  // Wartet auf User
  });

  return (/* ... */);
}

Mutations

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

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

  const createPost = useMutation({
    mutationFn: async (newPost: CreatePostInput) => {
      const response = await fetch('/api/posts', {
        method: 'POST',
        body: JSON.stringify(newPost)
      });
      if (!response.ok) throw new Error('Failed to create post');
      return response.json();
    },

    // Bei Erfolg: Cache invalidieren
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['posts'] });
    },

    // Oder: Optimistic Update
    onMutate: async (newPost) => {
      // Laufende Queries canceln
      await queryClient.cancelQueries({ queryKey: ['posts'] });

      // Snapshot des aktuellen States
      const previousPosts = queryClient.getQueryData(['posts']);

      // Optimistisch updaten
      queryClient.setQueryData(['posts'], (old: Post[]) => [
        { id: 'temp', ...newPost },
        ...old
      ]);

      return { previousPosts };
    },

    onError: (err, newPost, context) => {
      // Bei Fehler: Rollback
      queryClient.setQueryData(['posts'], context?.previousPosts);
    },

    onSettled: () => {
      // Immer am Ende: Refetch für Konsistenz
      queryClient.invalidateQueries({ queryKey: ['posts'] });
    }
  });

  const handleSubmit = (data: FormData) => {
    createPost.mutate({
      title: data.get('title') as string,
      content: data.get('content') as string
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="title" />
      <textarea name="content" />
      <button disabled={createPost.isPending}>
        {createPost.isPending ? 'Erstellen...' : 'Post erstellen'}
      </button>
      {createPost.isError && <Error message={createPost.error.message} />}
    </form>
  );
}

Pagination & Infinite Scroll

Pagination

function PaginatedPosts() {
  const [page, setPage] = useState(1);

  const { data, isPlaceholderData } = useQuery({
    queryKey: ['posts', page],
    queryFn: () => fetchPosts(page),
    placeholderData: keepPreviousData  // Zeigt alte Daten während neuer Page lädt
  });

  return (
    <div>
      {data?.posts.map(post => <PostCard key={post.id} post={post} />)}

      <div>
        <button
          onClick={() => setPage(p => Math.max(p - 1, 1))}
          disabled={page === 1}
        >
          Zurück
        </button>

        <span>Seite {page}</span>

        <button
          onClick={() => setPage(p => p + 1)}
          disabled={isPlaceholderData || !data?.hasMore}
        >
          Weiter
        </button>
      </div>
    </div>
  );
}

Infinite Scroll

import { useInfiniteQuery } from '@tanstack/react-query';
import { useInView } from 'react-intersection-observer';

function InfinitePosts() {
  const { ref, inView } = useInView();

  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage
  } = useInfiniteQuery({
    queryKey: ['posts', 'infinite'],
    queryFn: ({ pageParam }) => fetchPosts(pageParam),
    initialPageParam: 1,
    getNextPageParam: (lastPage) =>
      lastPage.hasMore ? lastPage.nextPage : undefined
  });

  // Auto-load bei Scroll
  useEffect(() => {
    if (inView && hasNextPage) {
      fetchNextPage();
    }
  }, [inView, hasNextPage, fetchNextPage]);

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

      <div ref={ref}>
        {isFetchingNextPage
          ? <Spinner />
          : hasNextPage
            ? 'Mehr laden...'
            : 'Keine weiteren Posts'}
      </div>
    </div>
  );
}

Prefetching

// Hover Prefetch
function PostLink({ postId }: { postId: string }) {
  const queryClient = useQueryClient();

  const prefetchPost = () => {
    queryClient.prefetchQuery({
      queryKey: ['post', postId],
      queryFn: () => fetchPost(postId),
      staleTime: 60 * 1000
    });
  };

  return (
    <Link
      href={`/posts/${postId}`}
      onMouseEnter={prefetchPost}
      onFocus={prefetchPost}
    >
      Post anzeigen
    </Link>
  );
}

// Server-Side Prefetch (Next.js)
export async function getServerSideProps() {
  const queryClient = new QueryClient();

  await queryClient.prefetchQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts
  });

  return {
    props: {
      dehydratedState: dehydrate(queryClient)
    }
  };
}

Query Invalidation

const queryClient = useQueryClient();

// Einzelnen Query invalidieren
queryClient.invalidateQueries({ queryKey: ['user', userId] });

// Alle User-Queries
queryClient.invalidateQueries({ queryKey: ['user'] });

// Alle Queries
queryClient.invalidateQueries();

// Mit Predicate
queryClient.invalidateQueries({
  predicate: (query) =>
    query.queryKey[0] === 'post' &&
    (query.queryKey[1] as Post)?.authorId === userId
});

// Nur refetchen wenn aktiv (sichtbar)
queryClient.invalidateQueries({
  queryKey: ['posts'],
  refetchType: 'active'  // 'all' | 'active' | 'inactive' | 'none'
});

Suspense Mode

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

function UserProfile({ userId }: { userId: string }) {
  // Wirft Promise während Loading (für Suspense)
  const { data: user } = useSuspenseQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId)
  });

  // user ist garantiert da (kein undefined)
  return <div>{user.name}</div>;
}

// Parent
function UserPage({ userId }: { userId: string }) {
  return (
    <Suspense fallback={<Skeleton />}>
      <UserProfile userId={userId} />
    </Suspense>
  );
}

Custom Hooks Pattern

// hooks/useUser.ts
export function useUser(userId: string) {
  return useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    enabled: !!userId
  });
}

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

  return useMutation({
    mutationFn: updateUser,
    onSuccess: (data, variables) => {
      queryClient.setQueryData(['user', variables.id], data);
    }
  });
}

// Verwendung
function Profile({ userId }: { userId: string }) {
  const { data: user, isLoading } = useUser(userId);
  const updateUser = useUpdateUser();

  // ...
}

DevTools & Debugging

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

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

<QueryClientProvider client={queryClient}>
  {children}
  <ReactQueryDevtools
    initialIsOpen={false}
    buttonPosition="bottom-right"
  />
</QueryClientProvider>

Fazit

TanStack Query bietet:

  1. Automatisches Caching: Keine manuelle Cache-Logik
  2. Background Refetch: Daten bleiben frisch
  3. Optimistic Updates: Schnelle UX
  4. DevTools: Debugging leicht gemacht

Für jede App mit Server-Daten ist TanStack Query unverzichtbar.


Bildprompts

  1. "Data flowing from server to UI with caching layer in between, architectural diagram"
  2. "Stale data refreshing in background, real-time update visualization"
  3. "Query cache tree with multiple components accessing same data, efficiency concept"

Quellen