In the dynamic world of modern web development, managing server state is one of the most significant challenges developers face. For years, the React community relied on a combination of useEffect
, useState
, and global state managers like Redux to handle asynchronous data fetching, caching, and synchronization. While functional, this approach often leads to complex boilerplate, manual state management, and subtle bugs. The latest React News highlights a paradigm shift towards dedicated server state libraries, and at the forefront of this movement is TanStack Query, formerly known as React Query. This powerful library provides a declarative and hook-based API to effortlessly manage server state, transforming how we build data-driven applications in React, Next.js, and even React Native.
This article offers a comprehensive exploration of TanStack Query. We’ll dive deep into its core concepts, demonstrate practical implementations with real-world code examples, and explore the underlying SQL operations that power our data fetching. Whether you’re building a blazing-fast SPA with Vite News, a server-rendered application with Next.js News or Remix News, or a mobile app with React Native News, understanding React Query is essential for writing clean, efficient, and resilient code.
Understanding the Core: Server State, Queries, and Mutations
Before diving into the code, it’s crucial to understand what “server state” is and why it needs a dedicated management tool. Unlike client state (e.g., a modal’s open/closed status, form inputs), server state is data that lives on a remote server. It’s asynchronous, shared across multiple users, and can become stale without our knowledge. React Query provides two primary hooks to interact with this state: useQuery
for reading data and useMutation
for creating, updating, or deleting it.
Fetching Data with useQuery
The useQuery
hook is the workhorse for fetching data. It requires two main arguments: a unique queryKey
to identify the data and a queryFn
that returns a promise (typically an API call). React Query handles the rest: caching, background refetching, and providing status indicators like isLoading
, isError
, and isSuccess
.
Imagine we’re building a news application. Our first task is to fetch a list of articles. Here’s how you would do it with useQuery
:
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
const fetchArticles = async () => {
const { data } = await axios.get('/api/articles');
return data;
};
function ArticleList() {
const { data: articles, isLoading, isError, error } = useQuery({
queryKey: ['articles', 'list'],
queryFn: fetchArticles,
});
if (isLoading) {
return <span>Loading articles...</span>;
}
if (isError) {
return <span>Error: {error.message}</span>;
}
return (
<ul>
{articles.map((article) => (
<li key={article.id}>{article.title}</li>
))}
</ul>
);
}
Behind the scenes, this simple hook triggers a request to our backend. The backend would then execute a SQL query to retrieve the data from a database.
The SQL Backend: Schema and Query

For our news application, we might have a simple PostgreSQL table to store articles. The schema could look like this:
-- Schema for our news articles table
CREATE TABLE articles (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
author_id INT REFERENCES authors(id),
status VARCHAR(20) DEFAULT 'draft',
published_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- An index to speed up queries by status and publication date
CREATE INDEX idx_articles_status_published_at ON articles (status, published_at DESC);
When our fetchArticles
function is called, the API endpoint would execute a SQL query to fetch all published articles, ordered by the most recent:
-- SQL query executed by the /api/articles endpoint
SELECT id, title, published_at
FROM articles
WHERE status = 'published'
ORDER BY published_at DESC
LIMIT 20;
Practical Implementation: Setup, Mutations, and Invalidation
To use React Query, you must wrap your application with a QueryClientProvider
. This provider gives all child components access to the QueryClient
instance, which manages the cache.
import React from 'react';
import ReactDOM from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from './App';
// Create a client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
cacheTime: 1000 * 60 * 30, // 30 minutes
},
},
});
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</React.StrictMode>
);
Modifying Data with useMutation
and Invalidating Cache
Reading data is only half the story. When a user creates, updates, or deletes an article, we need to inform React Query that the existing data in the cache is now stale. This is where useMutation
and query invalidation come in. The useMutation
hook gives you a mutate
function to trigger the change. In its onSuccess
callback, we can use the queryClient
to invalidate related queries, forcing them to refetch.
Let’s create a component to add a new article. This is a common pattern when working with libraries like React Hook Form News or Formik News.
import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';
const addArticle = async (newArticle) => {
const { data } = await axios.post('/api/articles', newArticle);
return data;
};
function AddArticleForm() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: addArticle,
onSuccess: () => {
// Invalidate and refetch the articles list
console.log('Article added successfully! Invalidating queries...');
queryClient.invalidateQueries({ queryKey: ['articles', 'list'] });
},
onError: (error) => {
console.error('Failed to add article:', error);
}
});
const handleSubmit = (event) => {
event.preventDefault();
const formData = new FormData(event.target);
const newArticle = {
title: formData.get('title'),
content: formData.get('content'),
};
mutation.mutate(newArticle);
};
return (
<form onSubmit={handleSubmit}>
{/* Form inputs for title and content */}
<input name="title" type="text" disabled={mutation.isLoading} />
<textarea name="content" disabled={mutation.isLoading} />
<button type="submit" disabled={mutation.isLoading}>
{mutation.isLoading ? 'Saving...' : 'Add Article'}
</button>
</form>
);
}
When the form is submitted, the addArticle
function sends a POST request. The backend would then perform an INSERT
operation. To ensure data integrity (e.g., updating multiple related tables), this operation should be wrapped in a transaction.
-- Using a transaction to ensure atomicity when creating an article
BEGIN;
-- Insert the new article and get its ID
INSERT INTO articles (title, content, author_id, status)
VALUES ('New Title from Form', 'Content from form...', 1, 'draft')
RETURNING id;
-- Potentially update another table, e.g., an author's article count
-- UPDATE authors SET article_count = article_count + 1 WHERE id = 1;
COMMIT;
Advanced Techniques for a Superior User Experience
React Query’s power extends far beyond basic fetching and mutating. Its advanced features, like optimistic updates and infinite queries, can dramatically improve the perceived performance and user experience of your application, whether it’s a web app using React Spring News for animations or a mobile app with React Navigation News.

Optimistic Updates for Instantaneous UI Feedback
An optimistic update is when you update the UI *before* the API call completes. The UI assumes the mutation will succeed, providing instant feedback. React Query makes this complex pattern manageable by providing callbacks to handle the entire lifecycle.
Let’s implement an optimistic update for toggling a “favorite” status on an article.
function ArticleActions({ article }) {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (isFavorited) => toggleFavoriteAPI(article.id, isFavorited),
// When the mutation is called:
onMutate: async (isFavorited) => {
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
await queryClient.cancelQueries({ queryKey: ['articles', article.id] });
// Snapshot the previous value
const previousArticle = queryClient.getQueryData(['articles', article.id]);
// Optimistically update to the new value
queryClient.setQueryData(['articles', article.id], {
...previousArticle,
isFavorited: isFavorited,
});
// Return a context object with the snapshotted value
return { previousArticle };
},
// If the mutation fails, use the context we returned to roll back
onError: (err, newTodo, context) => {
queryClient.setQueryData(['articles', article.id], context.previousArticle);
},
// Always refetch after error or success:
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['articles', article.id] });
},
});
return (
<button onClick={() => mutation.mutate(!article.isFavorited)}>
{article.isFavorited ? 'Unfavorite' : 'Favorite'}
</button>
);
}
Infinite Scrolling with useInfiniteQuery
For long lists of data, like a news feed, infinite scrolling is a common UX pattern. The useInfiniteQuery
hook is designed specifically for this. It works similarly to useQuery
but adds functionality to manage paginated data.
import { useInfiniteQuery } from '@tanstack/react-query';
const fetchPaginatedArticles = async ({ pageParam = 0 }) => {
const res = await axios.get(`/api/articles?page=${pageParam}`);
return res.data; // e.g., { articles: [...], nextPage: 1, hasMore: true }
};
function InfiniteArticleList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['articles', 'infinite'],
queryFn: fetchPaginatedArticles,
getNextPageParam: (lastPage, pages) => lastPage.nextPage ?? undefined,
});
return (
<div>
{data?.pages.map((page, i) => (
<React.Fragment key={i}>
{page.articles.map((article) => (
<p key={article.id}>{article.title}</p>
))}
</React.Fragment>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading more...' : hasNextPage ? 'Load More' : 'Nothing more to load'}
</button>
</div>
);
}
The backend for this would use SQL’s LIMIT
and OFFSET
clauses to fetch data in chunks.

-- SQL for fetching a "page" of data
-- The API would calculate the OFFSET based on the page number.
-- For page=0, OFFSET is 0. For page=1, OFFSET is 20.
SELECT id, title FROM articles WHERE status = 'published' ORDER BY published_at DESC LIMIT 20 OFFSET 0;
Best Practices and Optimization
To get the most out of React Query, follow these best practices. These tips are invaluable whether you’re working on a small project or a large-scale application, and they apply to testing with tools like React Testing Library News and Cypress News as well.
- Structure Your Query Keys: Use a consistent, serializable structure. A common pattern is an array starting with the entity name, followed by a type (list, detail), and then any unique identifiers. Example:
['articles', 'list', { category: 'tech' }]
or['articles', 'detail', 123]
. - Understand
staleTime
vs.cacheTime
:staleTime
is the duration until a query is considered stale. While data is fresh (not stale), React Query will return it from the cache without a refetch.cacheTime
is how long inactive data remains in the cache before being garbage collected. A longerstaleTime
(e.g., 5-10 minutes) is great for data that doesn’t change often, reducing unnecessary network requests. - Use the React Query Devtools: This is a non-negotiable for development. The devtools provide a visual interface to inspect your cache, see query states in real-time, and manually trigger actions. It’s an indispensable debugging tool.
- Co-locate Queries and Components: Keep your
useQuery
hooks close to the components that use them. For reusable queries, encapsulate them in custom hooks (e.g.,useArticles()
). - Combine with Client State Managers: React Query excels at server state. For client-side state, use it alongside lightweight managers like Zustand News or Jotai News, or more robust solutions like Redux News when needed. They solve different problems and work beautifully together.
Conclusion: The Future of Data Fetching in React
TanStack Query has fundamentally improved the developer experience of building data-driven applications in the React ecosystem. By abstracting away the complexities of caching, background synchronization, and state management, it allows developers to focus on building features rather than wrestling with asynchronous logic. Its declarative API, powerful features like optimistic updates, and first-class developer tools make it an essential library for any modern web or mobile application.
As you embark on your next project, whether it’s with Gatsby News, Blitz.js News, or a custom Expo News build, consider making TanStack Query your go-to solution for server state. By embracing its philosophy, you’ll write less code, build more resilient UIs, and deliver a faster, more responsive experience to your users. The React Query News is clear: the era of manual data fetching is over, and a more declarative, powerful future is here.