There’s little doubt that Tanstack Query is the modern go-to library for handling async data fetching and state management. Formerly known as React Query, Tanstack Query has seen significant adoption in modern web development, particularly within the React ecosystem, but also expanding to other frameworks like Vue, Solid, and Svelte. It abstracts away much of the complexity involved in fetching, caching, synchronizing, and updating server-side data in client-side applications. This includes handling loading states, errors, background re-fetching, and data invalidation. For all these reasons, it’s been widely adopted here at Atomic Object.
The Problem
Today I’d like to show you a lesser-known technique for reducing boilerplate code when performing a mutation operation. A common pattern you might see in a typical client-side application is this:
const mutation = useMutation({
mutationFn: createTodo,
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
toast.success("Todo created successfullu")
},
onError: (error) => {
toast.error("Something went wrong white creating todo!")
},
});
As you can see, as you write more mutations there will be a lot of repeated code: the onSuccess
/ onError
callback function, the cache invalidation and the triggering of the error or success toast messages.
A Solution
This can all be simplified with a slightly different approach involving some global configuration. Using a custom MutationCache object and configuring it’s onSuccess
and onError
callbacks to display the toasts with a custom message specified in your useMutation
call. You can also specify when queries to invalidate
import {
MutationCache,
QueryClient,
QueryKey,
} from "@tanstack/react-query";
import { toast } from "sonner";
declare module "@tanstack/react-query" {
interface Register {
mutationMeta: {
skipToast?: boolean;
invalidatesQuery?: QueryKey;
successMessage?: string;
successTitle?: string;
errorMessage?: string;
errorTitle?: string;
};
}
}
export const queryClient = new QueryClient({
mutationCache: new MutationCache({
onSuccess: (_data, _variables, _context, mutation) => {
if (mutation.meta?.successMessage && !mutation.meta?.skipToast) {
toast.success(mutation.meta.successMessage);
}
},
onError: (_error, _variables, _context, mutation) => {
console.log("Mutation error:", _error);
if (mutation.meta?.errorMessage && !mutation.meta?.skipToast) {
toast.error(mutation.meta.errorMessage);
}
},
onSettled: (_data, _error, _variables, _context, mutation) => {
if (mutation.meta?.invalidatesQuery) {
queryClient.invalidateQueries({
queryKey: mutation.meta.invalidatesQuery,
});
}
},
}),
});
The Solution In Action
Now, when performing a mutation, your code becomes much cleaner and more declarative:
const mutation = useMutation({
mutationFn: createTodo,
meta: {
invalidatesQuery: ['todos'],
successMessage: 'Todo created successfully',
errorMessage: 'Something when wrong while creating todo!'
}
});
Or, should you wish to skip showing the toast messages entirely in some cases:
const mutation = useMutation({
mutationFn: createTodo,
meta: {
invalidatesQuery(['todos']),
skipToast: true,
}
});
Benefits and Extensions
This pattern offers several advantages:
- Consistency: All mutations follow the same pattern for error handling and cache invalidation
- Maintainability: Changes to toast behavior or invalidation logic can be made in one place
- Flexibility: Individual mutations can still opt out of global behavior when needed
- Cleaner Code: Mutation definitions focus on their core purpose rather than cross-cutting concerns
With a few additional changes, this setup could be extended to invalidate multiple queries, specify toast titles along with custom messages, or even include more sophisticated error handling logic. This makes it a very powerful pattern to have in your toolkit when building web applications that use TanStack Query with several mutations and toast notifications.
The post Globally Manage Toast Notifications with Tanstack Query appeared first on Atomic Spin.