Before anything I would like to say, I aspire to become someone who knows and cares about perf, I also care about developer experience.
Which brings us to TanStack Query a library that, at least to me, shares the same priorities. Without TanStack Query, the responsibility for managing asynchronous data fetching, caching, and state synchronization falls directly on the developer. That means repetitive plumbing .
I think a framework or library should remove configuration overhead so developers can focus on building features as quickly as possible.
At a glance , core concepts
- Query: a description of remote data (use
useQueryto fetch + cache it). - Query key: a unique identifier for a query (e.g.
['todos', userId]). - Cache: TanStack Query keeps cached results and updates UI efficiently.
- Mutations: use
useMutationfor writes; coordinate the cache with invalidation or optimistic updates. - SSR hydration:
dehydrateon the server andHydrateon the client to avoid double fetching.
The problem (short)
build a small todo widget: fetch the list, show loading states, and add items. The naive approach copies fetch logic and loading/error handling across components and results in repeated refetches or stale UI. TanStack Query centralizes this so your components become simple and declarative.
Before vs After
Here are two compact React examples. The first is manual fetching and state. The second uses TanStack Query and is far cleaner for shared server state.
This is what many of us write first. It works, but it quickly becomes repetitive.
// TodosManual.jsx
import { useState, useEffect } from 'react';
function TodosManual() {
const [todos, setTodos] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/todos')
.then(res => res.json())
.then(data => setTodos(data))
.finally(() => setLoading(false));
}, []);
if (loading) return <div>Loading…</div>;
return (
<ul>
{todos.map(t => <li key={t.id}>{t.text}</li>)}
</ul>
);
Problems you'll hit:
- repeated network re-fetches across components
- duplicated loading/error handling
- manual optimistic updates and rollbacks that are easy to get wrong
using TanStack Query
This approach moves fetching and cache coordination into a shared layer.
// TodosWithTanStack.jsx
import { useQuery } from '@tanstack/react-query';
function fetchTodos() {
return fetch('/api/todos').then((res) => res.json());
}
function TodosWithQuery() {
const { data: todos = [], isLoading } = useQuery(['todos'], fetchTodos);
if (isLoading) return <div>Loading…</div>;
return (
<ul>
{todos.map((t) => (
<li key={t.id}>{t.text}</li>
))}
</ul>
);
}
Why this is better:
- No need for useState or useEffect for fetching.
- Loading states are handled automatically.
- Queries are cached, so repeated requests are efficient.
PS
TanStack Query is small to adopt and gives disproportionate wins for apps with shared server state. I am not yet know it all about tanstack query person but it seems to handle data that get fetched from server smartly so I am intrested to explore what could be achieved more with this !!