In the ever-evolving landscape of frontend development, libraries and frameworks constantly refine their APIs to promote better patterns, improve performance, and enhance developer experience. TanStack Query (formerly React Query), a cornerstone for data fetching and server-state management in the React ecosystem, is no exception. A recent significant change in its v5 release has sparked discussions across the community: the removal of the onSuccess, onError, and onSettled callback options from the useQuery hook. This is major React Query News and a shift that encourages a more declarative and React-idiomatic approach to handling side effects.
While this change might seem disruptive at first, it’s rooted in sound principles that align with React’s core philosophy. It nudges developers away from imperative callbacks and towards deriving behavior from state. This article provides a comprehensive guide to understanding why this change was made, how to adapt your existing code, and how to leverage the new patterns for building more robust and predictable applications. We’ll explore practical code examples, advanced techniques, and even touch upon the backend database interactions that power our frontend queries, making this relevant for anyone following React News and modern development practices in frameworks like Next.js, Remix, or Gatsby.
Understanding the “Why”: A Philosophical Shift from Imperative to Declarative
The decision to deprecate side-effect callbacks in useQuery wasn’t arbitrary. It addresses several subtle but important issues inherent in the old pattern and promotes a style of coding that is more resilient and easier to reason about.
The Pitfalls of Inline Side-Effect Callbacks
The previous API, while convenient, could lead to a few common problems:
- Stale Closures: Callbacks defined inside a component can capture variables from their surrounding scope (a “closure”). If the component re-renders but the query isn’t refetched, the callback might hold onto stale props or state, leading to unpredictable bugs. While this could be managed with tools like
useCallback, it added extra complexity. - Mixing Concerns:
useQuery‘s primary responsibility is to fetch, cache, and manage server state. Tying side effects like showing a toast notification or redirecting a user directly into the data-fetching hook couples two distinct concerns. This makes the code harder to read, test, and maintain. - Divergence from React’s Lifecycle: The modern React paradigm, heavily influenced by hooks, emphasizes that side effects should be a reaction to state changes. The ideal place for this is within a
useEffecthook, which is explicitly designed to synchronize your component with an external system (like the browser DOM, a notification library, or the navigation API). Triggering side effects from a data-fetching callback breaks this declarative model.
Setting the Stage: A Database Schema for Our News App
To make our examples concrete, let’s assume we’re building a news application. Our frontend will need to fetch articles from a backend database. Here’s a simple SQL schema for our articles table, which we’ll query against throughout this article.
-- Define the schema for our news articles table
CREATE TABLE news_articles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
category VARCHAR(50) CHECK (category IN ('Technology', 'Business', 'Sports', 'Health')),
author_id UUID REFERENCES users(id),
publish_date TIMESTAMPTZ DEFAULT NOW(),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- An index to speed up queries by category
CREATE INDEX idx_articles_category ON news_articles(category);
Practical Migration Patterns: From Callbacks to Effects
The new recommended approach is to use the state returned by useQuery (like isSuccess, isError, data, and error) within a useEffect hook to trigger side effects. Let’s look at how to migrate common patterns.
Use Case 1: Displaying Toast Notifications

TanStack React Query logo – TypeScript Masterclass 2025 Edition – React + NodeJS Project | Udemy
A frequent use case is showing a success or error message after a fetch completes. This is a classic side effect that now belongs in useEffect.
The Old Way (React Query v4):
import { useQuery } from '@tanstack/react-query';
import { toast } from 'react-toastify';
import { fetchArticles } from './api'; // Assume this function fetches data
function ArticleList() {
const { data, isLoading } = useQuery({
queryKey: ['articles'],
queryFn: fetchArticles,
onSuccess: (data) => {
toast.success(`Successfully fetched ${data.length} articles!`);
},
onError: (error) => {
toast.error(`Failed to fetch articles: ${error.message}`);
},
});
// ... render logic
}
The New, Recommended Way (React Query v5):
In the new pattern, we use useEffect and listen for changes in the isSuccess or isError state. It’s crucial to use a dependency that doesn’t change on every render, such as the stable data or error objects themselves.
import { useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import { toast } from 'react-toastify';
import { fetchArticles } from './api';
function ArticleList() {
const { data, isSuccess, isError, error, isLoading } = useQuery({
queryKey: ['articles'],
queryFn: fetchArticles,
});
useEffect(() => {
if (isSuccess && data) {
toast.success(`Successfully fetched ${data.length} articles!`);
}
}, [isSuccess, data]); // Depend on data to ensure effect runs only when new data arrives
useEffect(() => {
if (isError && error) {
toast.error(`Failed to fetch articles: ${error.message}`);
}
}, [isError, error]); // Depend on the error object
// ... render logic
}
The SQL Query Behind the Fetch
Our fetchArticles API function would execute a SQL query like this against the database to retrieve the data.
-- The SQL query executed by our API to fetch recent technology articles
SELECT id, title, category, publish_date
FROM news_articles
WHERE category = 'Technology'
ORDER BY publish_date DESC
LIMIT 20;
Advanced Techniques and The Role of Mutations
While useQuery has changed, it’s important to understand where callbacks still have a place and how to manage global side effects for your entire application, which is essential Next.js News and Remix News for developers building full-stack applications.
useMutation: Where Callbacks Still Reign
Crucially, this change only applies to useQuery. The useMutation hook, which is used for creating, updating, or deleting data, retains its onSuccess, onError, and onSettled callbacks. This makes perfect sense because mutations are imperative by nature. You explicitly trigger a mutation (e.g., when a user clicks a “Save” button), and you often need to perform an action immediately upon its completion, like invalidating queries, redirecting the user, or showing a confirmation message.
Here’s an example of creating a new news article using useMutation.
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { createNewArticle } from './api'; // API function to POST new article
import { toast } from 'react-toastify';
import { useRouter } from 'next/router'; // Example for Next.js
function NewArticleForm() {
const queryClient = useQueryClient();
const router = useRouter();
const mutation = useMutation({
mutationFn: createNewArticle,
onSuccess: (newArticle) => {
// Invalidate and refetch the articles list
queryClient.invalidateQueries({ queryKey: ['articles'] });
toast.success('Article created successfully!');
// Navigate to the new article's page
router.push(`/articles/${newArticle.id}`);
},
onError: (error) => {
toast.error(`Creation failed: ${error.message}`);
},
});
const handleSubmit = (event) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const articleData = Object.fromEntries(formData.entries());
mutation.mutate(articleData);
};
// ... form JSX
}
The SQL Transaction for Creating an Article
TanStack React Query logo – Mastering Maintainable React | Udemy
The createNewArticle API function on the server would likely wrap its database operations in a transaction to ensure data integrity. For instance, it might insert the article and update a user’s article count simultaneously.
BEGIN;
-- Insert the new article into the table
INSERT INTO news_articles (title, content, category, author_id)
VALUES ('New React Features', 'React 19 introduces...', 'Technology', 'user-uuid-123')
RETURNING id; -- Return the new ID for the frontend to use
-- Atomically update a related table, e.g., the author's article count
UPDATE users
SET article_count = article_count + 1
WHERE id = 'user-uuid-123';
COMMIT;
Global Callbacks with QueryClient
If you need to run a side effect for every single query in your application (e.g., for logging or global error reporting), you can configure this on the QueryClient itself. This is a powerful pattern for cross-cutting concerns.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { logErrorToService } from './error-logger';
// Create a client
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error) => {
// This will fire for every failed query globally
console.error(`Something went wrong: ${error.message}`);
logErrorToService(error);
},
}),
});
function App() {
return (
<QueryClientProvider client={queryClient}>
{/* The rest of your app */}
</QueryClientProvider>
);
}
Best Practices and Optimization
Adopting this new pattern not only aligns your code with React principles but also opens the door for cleaner, more optimized components. Keeping up with these practices is vital for anyone following React Native News or state management trends like Zustand News and Redux News, as these principles often apply across the ecosystem.
Embrace Declarative Rendering
The most common and powerful use of React Query’s status flags is for conditional rendering directly in your JSX. This is the most declarative pattern of all and should be your default approach.
TanStack React Query logo – react-query-kit – npm
function ArticleView({ articleId }) {
const { data, isLoading, isError, error } = useQuery({
queryKey: ['article', articleId],
queryFn: () => fetchArticleById(articleId),
});
if (isLoading) {
return <div>Loading article...</div>;
}
if (isError) {
return <div>Error: {error.message}</div>;
}
return (
<article>
<h1>{data.title}</h1>
<p>{data.content}</p>
</article>
);
}
Keep `queryFn` Pure
Your queryFn should have one job: to fetch data and return a promise that resolves with that data or rejects with an error. It should never contain side effects like setting state, navigating, or showing alerts. This separation of concerns is critical for predictability and testability, a topic often discussed in React Testing Library News.
Custom Hooks for Reusable Logic
If you find yourself repeating the same useEffect logic across multiple components, encapsulate it in a custom hook. This keeps your component code clean and your side-effect logic centralized and reusable.
import { useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import { toast } from 'react-toastify';
// Custom hook that wraps useQuery and adds toast notifications
export function useNotifyQuery(queryKey, queryFn, options) {
const queryResult = useQuery({ queryKey, queryFn, ...options });
const { isError, error, isSuccess } = queryResult;
useEffect(() => {
if (isError && error) {
toast.error(error.message);
}
}, [isError, error]);
useEffect(() => {
if (isSuccess) {
toast.info('Data fetched!');
}
}, [isSuccess]);
return queryResult;
}
Conclusion: Embracing a More Resilient Pattern
The removal of onSuccess, onError, and onSettled from useQuery in TanStack Query v5 is more than just an API change; it’s a deliberate step towards writing more declarative, predictable, and maintainable React applications. By shifting side effects from imperative callbacks to the declarative useEffect hook, we better align our data-fetching logic with React’s core rendering lifecycle. This reduces the risk of stale closures and more clearly separates the concern of fetching data from the concern of reacting to it.
While this requires a mental shift and some code migration, the result is a cleaner architecture that is easier to reason about and debug. Mutations rightly keep their callbacks for imperative actions, and global configurations on the QueryClient provide a home for application-wide side effects. As developers, embracing these evolving best practices ensures our applications are not only functional but also robust, scalable, and in tune with the modern React ecosystem.












