DEV Community

Alex Spinov
Alex Spinov

Posted on

TanStack Query Has a Free Data Fetching Library: Automatic Caching, Background Refetching, and Infinite Scroll Built In

You fetch data with useEffect. You manage loading, error, and success states manually. You cache nothing — every page navigation re-fetches everything. Users see loading spinners constantly.

What if your data fetching library handled caching, deduplication, background updates, pagination, and optimistic updates — automatically?

That's TanStack Query (formerly React Query).

Before and After

Without TanStack Query

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    fetch("/api/users")
      .then(r => r.json())
      .then(data => { if (!cancelled) { setUsers(data); setLoading(false); } })
      .catch(err => { if (!cancelled) { setError(err); setLoading(false); } });
    return () => { cancelled = true; };
  }, []);

  if (loading) return <Spinner />;
  if (error) return <Error message={error.message} />;
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
Enter fullscreen mode Exit fullscreen mode

With TanStack Query

function UserList() {
  const { data: users, isLoading, error } = useQuery({
    queryKey: ["users"],
    queryFn: () => fetch("/api/users").then(r => r.json()),
  });

  if (isLoading) return <Spinner />;
  if (error) return <Error message={error.message} />;
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
Enter fullscreen mode Exit fullscreen mode

But the real difference isn't code length — it's behavior:

  • Automatic caching — navigate away and back, data shows instantly
  • Background refetching — stale data shows while fresh data loads
  • Deduplication — 5 components request "users" = 1 API call
  • Window focus refetching — user tabs back, data refreshes automatically

Setup

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

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60_000,      // Data fresh for 1 minute
      gcTime: 300_000,         // Cache kept for 5 minutes
      retry: 3,                // Retry failed requests 3 times
      refetchOnWindowFocus: true,
    },
  },
});

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <MyApp />
    </QueryClientProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Mutations With Optimistic Updates

function TodoItem({ todo }) {
  const queryClient = useQueryClient();

  const toggleMutation = useMutation({
    mutationFn: (id: string) => 
      fetch(`/api/todos/${id}/toggle`, { method: "PATCH" }).then(r => r.json()),

    // Optimistic update — instant UI feedback
    onMutate: async (id) => {
      await queryClient.cancelQueries({ queryKey: ["todos"] });
      const previous = queryClient.getQueryData(["todos"]);

      queryClient.setQueryData(["todos"], (old: Todo[]) =>
        old.map(t => t.id === id ? { ...t, done: !t.done } : t)
      );

      return { previous };
    },

    // Rollback on error
    onError: (err, id, context) => {
      queryClient.setQueryData(["todos"], context.previous);
    },

    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ["todos"] });
    },
  });

  return (
    <li onClick={() => toggleMutation.mutate(todo.id)}>
      {todo.done ? "" : ""} {todo.text}
    </li>
  );
}
Enter fullscreen mode Exit fullscreen mode

Infinite Scroll

function Feed() {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
    queryKey: ["feed"],
    queryFn: ({ pageParam }) => 
      fetch(`/api/feed?cursor=${pageParam}`).then(r => r.json()),
    initialPageParam: "",
    getNextPageParam: (lastPage) => lastPage.nextCursor,
  });

  return (
    <div>
      {data?.pages.flatMap(page => page.items).map(item => (
        <FeedItem key={item.id} item={item} />
      ))}
      {hasNextPage && (
        <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
          {isFetchingNextPage ? "Loading..." : "Load More"}
        </button>
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Dependent Queries

function UserProfile({ userId }) {
  const userQuery = useQuery({
    queryKey: ["user", userId],
    queryFn: () => fetchUser(userId),
  });

  const postsQuery = useQuery({
    queryKey: ["posts", userId],
    queryFn: () => fetchUserPosts(userId),
    enabled: !!userQuery.data, // Only runs after user loads
  });

  return (
    <div>
      <h1>{userQuery.data?.name}</h1>
      {postsQuery.data?.map(post => <PostCard key={post.id} post={post} />)}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

When to Choose TanStack Query

Choose TanStack Query when:

  • Your app fetches server data (any app with API calls)
  • You want automatic caching and background updates
  • Pagination or infinite scroll is needed
  • You want to eliminate manual loading/error state management

Skip TanStack Query when:

  • Client-only state (use Zustand/Jotai instead)
  • Very simple app with 1-2 fetch calls (overkill)
  • You use a framework with built-in data layer (Remix loaders, SvelteKit load)

The Bottom Line

TanStack Query turns server state from a problem you solve to a problem that's solved. Caching, deduplication, background updates, optimistic mutations — you get production-grade data fetching without writing the infrastructure.

Start here: tanstack.com/query


Need custom data extraction, scraping, or automation? I build tools that collect and process data at scale — 78 actors on Apify Store and 265+ open-source repos. Email me: Spinov001@gmail.com | My Apify Actors

Top comments (0)