Welcome back, intrepid learner! In Chapter 4, we became adept at fetching server data using useQuery and understood how TanStack Query automatically caches and keeps our UI fresh. But what happens when our application isn’t just reading data, but changing it? Think about creating a new post, updating a user’s profile, or deleting an item from a list. These actions are called “mutations” – they modify data on the server.
In this chapter, we’ll dive deep into useMutation, TanStack Query’s powerful hook for interacting with server-side data modifications. We’ll learn how to trigger these changes, how to ensure our user interface (UI) reflects these changes accurately, and most excitingly, how to make our app feel incredibly fast and responsive using “optimistic updates.” By the end, you’ll be able to build highly interactive and performant applications that seamlessly manage server state changes. Ready to level up your data management skills? Let’s go!
Core Concepts
When you’re building interactive applications, displaying data is only half the battle. Users expect to be able to create, update, and delete information. This is where the concept of “mutations” comes into play.
Understanding Mutations with useMutation
In TanStack Query, useQuery is for fetching data (GET requests), while useMutation is for performing side effects on the server (like POST, PUT, DELETE requests). These operations fundamentally change data, which has a ripple effect on our client-side cache.
Think of it like this:
useQuery: You’re asking a librarian for a book. They give you a copy, and you read it. The original book (server data) remains unchanged.useMutation: You’re writing a new book, editing an existing one, or throwing one away. These actions change the library’s collection (server data).
The useMutation hook provides functions and state to manage the lifecycle of these server operations:
mutate(ormutateAsync): The function you call to trigger the mutation.isPending: A boolean indicating if the mutation is currently in progress.isError: A boolean indicating if the mutation failed.isSuccess: A boolean indicating if the mutation succeeded.data: The data returned by the mutation function on success.error: The error object if the mutation failed.
Query Invalidation: The “Refresh” Button for Your Data
After you successfully change data on the server using a mutation, your client-side cache for that data might become stale. For example, if you add a new todo item, your useQuery hook fetching “all todos” still has the old list in its cache, missing the new item.
This is where Query Invalidation comes in. It’s the process of marking cached query data as “stale,” forcing TanStack Query to refetch that data from the server the next time it’s accessed or observed.
Why is this important? Because it ensures your UI always reflects the most up-to-date server state without you having to manually manage complex state updates. TanStack Query takes care of the “when” and “how” of refetching.
The primary tool for invalidation is queryClient.invalidateQueries(). You can target specific queries by their queryKey.
Figure 5.1: Flow of a Mutation with Query Invalidation
Optimistic Updates: The “Instant Feedback” Superpower
Even with query invalidation, there’s still a brief moment of loading while the mutation completes on the server and the subsequent refetch occurs. For a truly seamless user experience, we can employ Optimistic Updates.
Optimistic updates mean you update the UI immediately after a user action, before the server has confirmed the change. You “optimistically” assume the server operation will succeed. This makes your application feel incredibly fast and responsive.
However, there’s a catch: what if the server operation fails? In that case, you need a mechanism to “rollback” the optimistic UI update and revert to the previous state. TanStack Query provides robust tools within useMutation to handle this:
onMutate: This callback fires before the mutation function is even sent to the server. It’s the perfect place to:- Cancel any ongoing queries that might interfere.
- Take a snapshot of the current query cache data (for potential rollback).
- Update the query cache optimistically with the expected new data.
onError: This callback fires if the mutation fails. Here, you use the snapshot taken inonMutateto rollback the cache to its previous state.onSettled: This callback fires regardless of whether the mutation succeeded or failed. It’s a great place to invalidate and refetch the relevant queries, ensuring the UI eventually reflects the true server state.
Optimistic updates are a powerful advanced pattern, but they require careful implementation to handle error scenarios gracefully.
Step-by-Step Implementation
Let’s put these concepts into practice by building a simple Todo list application. We’ll start with fetching todos, then add functionality to create new todos with both query invalidation and optimistic updates.
First, let’s set up a mock API to simulate network requests. Create a file named src/mockApi.ts:
// src/mockApi.ts
interface Todo {
id: string;
title: string;
completed: boolean;
}
// Our "server-side" data store
let todos: Todo[] = [
{ id: '1', title: 'Learn TanStack Query basics', completed: false },
{ id: '2', title: 'Master mutations', completed: false },
{ id: '3', title: 'Implement optimistic updates', completed: false },
];
// Helper to simulate network latency
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
export const mockApi = {
fetchTodos: async (): Promise<Todo[]> => {
await sleep(500); // Simulate 0.5 second network delay
console.log('API: Fetched todos', todos);
return [...todos]; // Return a copy to prevent direct modification
},
addTodo: async (title: string): Promise<Todo> => {
await sleep(700); // Simulate 0.7 second network delay
if (Math.random() < 0.2) { // 20% chance of failure for testing rollback
console.error('API: Failed to add todo (simulated error)');
throw new Error('Failed to add todo on server!');
}
const newTodo: Todo = {
id: String(Date.now()), // Simple unique ID
title,
completed: false,
};
todos.push(newTodo);
console.log('API: Added todo', newTodo);
return newTodo;
},
deleteTodo: async (id: string): Promise<void> => {
await sleep(600);
if (Math.random() < 0.15) { // 15% chance of failure
console.error('API: Failed to delete todo (simulated error)');
throw new Error('Failed to delete todo on server!');
}
todos = todos.filter(todo => todo.id !== id);
console.log('API: Deleted todo with ID', id);
return;
},
// We'll add updateTodo in the mini-challenge or as an extension.
};
This mockApi.ts file exports functions that mimic API calls, complete with simulated network delays and occasional errors to help us test our optimistic update rollback logic.
Next, ensure your src/main.tsx (or src/index.tsx) is set up with QueryClientProvider as we did in Chapter 4:
// src/main.tsx (or src/index.tsx)
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import './index.css';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; // v5
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; // v5
// Create a client
const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
{/* Devtools are incredibly helpful for debugging TanStack Query! */}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</React.StrictMode>,
);
Step 1: Fetching Todos with useQuery (Review)
Let’s create our main TodoList component that fetches and displays todos. This should be familiar from Chapter 4.
Create src/App.tsx:
// src/App.tsx
import React from 'react';
import { useQuery } from '@tanstack/react-query'; // v5
import { mockApi } from './mockApi'; // Our mock API
interface Todo {
id: string;
title: string;
completed: boolean;
}
function TodoList() {
// Use useQuery to fetch the list of todos
const { data, isPending, isError, error } = useQuery<Todo[], Error>({
queryKey: ['todos'], // A unique key for this query
queryFn: mockApi.fetchTodos, // The function that fetches the data
staleTime: 5 * 60 * 1000, // Data is considered fresh for 5 minutes
});
if (isPending) {
return <p>Loading todos...</p>;
}
if (isError) {
return <p>Error loading todos: {error?.message}</p>;
}
return (
<div>
<h1>My Todo List (Fetched)</h1>
<ul>
{data?.map(todo => (
<li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.title}
</li>
))}
</ul>
</div>
);
}
function App() {
return (
<div style={{ fontFamily: 'sans-serif', padding: '20px' }}>
<TodoList />
</div>
);
}
export default App;
Run your application. You should see “Loading todos…” briefly, then a list of todos. This confirms our basic data fetching is working. Great start!
Step 2: Adding a Todo with useMutation and Invalidation
Now, let’s add a form to create new todo items. We’ll use useMutation for this and then invalidate the todos query to refetch the updated list from the server.
First, add a new component AddTodoForm and integrate it into App.tsx:
// src/App.tsx (Update this file)
import React, { useState } from 'react'; // Import useState
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; // Import useMutation, useQueryClient
import { mockApi } from './mockApi';
interface Todo {
id: string;
title: string;
completed: boolean;
}
// New component for adding todos
function AddTodoForm() {
const [newTodoTitle, setNewTodoTitle] = useState('');
const queryClient = useQueryClient(); // Get the QueryClient instance
// Use useMutation for adding a todo
const addTodoMutation = useMutation<Todo, Error, string>({
mutationFn: mockApi.addTodo, // The function that performs the API call
onSuccess: () => {
// When the mutation is successful, invalidate the 'todos' query
// This tells TanStack Query that the 'todos' data is now stale and needs to be refetched.
queryClient.invalidateQueries({ queryKey: ['todos'] });
console.log('Mutation successful! Todos query invalidated.');
setNewTodoTitle(''); // Clear the input field
},
onError: (error) => {
console.error('Failed to add todo:', error.message);
alert(`Error: ${error.message}`); // Simple error feedback
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (newTodoTitle.trim()) {
addTodoMutation.mutate(newTodoTitle); // Trigger the mutation
}
};
return (
<form onSubmit={handleSubmit} style={{ margin: '20px 0', padding: '15px', border: '1px solid #ccc', borderRadius: '8px' }}>
<input
type="text"
value={newTodoTitle}
onChange={(e) => setNewTodoTitle(e.target.value)}
placeholder="New todo title"
disabled={addTodoMutation.isPending} // Disable input while mutation is pending
style={{ padding: '8px', marginRight: '10px', borderRadius: '4px', border: '1px solid #ddd' }}
/>
<button
type="submit"
disabled={addTodoMutation.isPending} // Disable button while mutation is pending
style={{ padding: '8px 15px', borderRadius: '4px', border: 'none', backgroundColor: '#007bff', color: 'white', cursor: 'pointer' }}
>
{addTodoMutation.isPending ? 'Adding...' : 'Add Todo'}
</button>
{addTodoMutation.isError && <p style={{ color: 'red' }}>Error adding todo!</p>}
{addTodoMutation.isSuccess && <p style={{ color: 'green' }}>Todo added successfully!</p>}
</form>
);
}
function TodoList() {
const { data, isPending, isError, error } = useQuery<Todo[], Error>({
queryKey: ['todos'],
queryFn: mockApi.fetchTodos,
staleTime: 5 * 60 * 1000,
});
if (isPending) {
return <p>Loading todos...</p>;
}
if (isError) {
return <p>Error loading todos: {error?.message}</p>;
}
return (
<div>
<h1>My Todo List (Fetched)</h1>
<ul>
{data?.map(todo => (
<li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.title}
</li>
))}
</ul>
</div>
);
}
function App() {
return (
<div style={{ fontFamily: 'sans-serif', padding: '20px' }}>
<TodoList />
<AddTodoForm /> {/* Add the new form here */}
</div>
);
}
export default App;
Explanation of new code:
import { useMutation, useQueryClient } from '@tanstack/react-query';: We importuseMutationfor our server-side changes anduseQueryClientto get access to thequeryClientinstance, which is essential for invalidating queries.const queryClient = useQueryClient();: This hook gives us theQueryClientinstance from theQueryClientProviderwe set up inmain.tsx.const addTodoMutation = useMutation(...): This is where the magic happens.mutationFn: mockApi.addTodo: We telluseMutationwhich asynchronous function to call whenmutateis triggered. This function receives the payload (the new todo title in this case) and should return a Promise.onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }); ... }: This callback executes aftermockApi.addTodosuccessfully resolves. Inside it,queryClient.invalidateQueries({ queryKey: ['todos'] })tells TanStack Query: “Hey, the data associated with the['todos']key might have changed. Mark it as stale so it gets refetched next time it’s needed.” Since ourTodoListcomponent is observing['todos'], it will automatically refetch and update.onError: A callback to handle errors ifmockApi.addTodorejects.
addTodoMutation.mutate(newTodoTitle);: When the form is submitted, we callmutatewith the new todo’s title. This triggers the mutation function (mockApi.addTodo).addTodoMutation.isPending: We use this state to disable the input and button while the API call is in progress, preventing duplicate submissions and providing visual feedback.
Now, try adding a new todo! You’ll notice a slight delay (due to sleep in mockApi.ts), then the new todo appears in the list. If you open your browser’s developer console, you’ll see messages from mockApi and console.log indicating the mutation success and query invalidation.
Step 3: Implementing Optimistic Updates for Adding a Todo
While the previous step works, there’s a noticeable delay before the new todo appears. Let’s make it feel instant using optimistic updates! We’ll modify our AddTodoForm to incorporate onMutate, onError, and onSettled.
// src/App.tsx (Update AddTodoForm again)
import React, { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { mockApi } from './mockApi';
interface Todo {
id: string;
title: string;
completed: boolean;
}
function AddTodoForm() {
const [newTodoTitle, setNewTodoTitle] = useState('');
const queryClient = useQueryClient();
const addTodoMutation = useMutation<Todo, Error, string, { previousTodos: Todo[] }>({ // Added a fourth generic type for context
mutationFn: mockApi.addTodo,
onMutate: async (newTodoTitle: string) => {
// 1. Cancel any outgoing refetches (so they don't overwrite our optimistic update)
await queryClient.cancelQueries({ queryKey: ['todos'] });
// 2. Snapshot the previous value
// This is crucial for rolling back if the mutation fails.
const previousTodos = queryClient.getQueryData<Todo[]>(['todos']);
// 3. Optimistically update the cache with the new value
if (previousTodos) {
queryClient.setQueryData<Todo[]>(['todos'], (old) => [
...(old || []),
{ id: 'optimistic-id-' + Date.now(), title: newTodoTitle, completed: false }, // Temporary ID
]);
}
console.log('Optimistic update: Added temporary todo.');
// 4. Return a context object with the snapshot value
// This context will be passed to onError and onSettled.
return { previousTodos: previousTodos || [] };
},
onError: (err, newTodoTitle, context) => {
// 5. If the mutation fails, use the context to roll back the cache
console.error('Optimistic update failed, rolling back:', err.message);
queryClient.setQueryData(['todos'], context?.previousTodos);
alert(`Error adding todo: ${err.message}. Rolling back.`);
},
onSettled: () => {
// 6. Always refetch after error or success to ensure client state is in sync with server
queryClient.invalidateQueries({ queryKey: ['todos'] });
console.log('Mutation settled. Todos query invalidated (for refetch).');
setNewTodoTitle(''); // Clear the input field
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (newTodoTitle.trim()) {
addTodoMutation.mutate(newTodoTitle);
}
};
return (
<form onSubmit={handleSubmit} style={{ margin: '20px 0', padding: '15px', border: '1px solid #ccc', borderRadius: '8px' }}>
<input
type="text"
value={newTodoTitle}
onChange={(e) => setNewTodoTitle(e.target.value)}
placeholder="New todo title"
disabled={addTodoMutation.isPending}
style={{ padding: '8px', marginRight: '10px', borderRadius: '4px', border: '1px solid #ddd' }}
/>
<button
type="submit"
disabled={addTodoMutation.isPending}
style={{ padding: '8px 15px', borderRadius: '4px', border: 'none', backgroundColor: '#007bff', color: 'white', cursor: 'pointer' }}
>
{addTodoMutation.isPending ? 'Adding...' : 'Add Todo'}
</button>
{addTodoMutation.isError && <p style={{ color: 'red' }}>Error adding todo!</p>}
{/* No success message needed here, as the UI updates optimistically */}
</form>
);
}
// TodoList and App components remain the same as before
function TodoList() {
const { data, isPending, isError, error } = useQuery<Todo[], Error>({
queryKey: ['todos'],
queryFn: mockApi.fetchTodos,
staleTime: 5 * 60 * 1000,
});
if (isPending) {
return <p>Loading todos...</p>;
}
if (isError) {
return <p>Error loading todos: {error?.message}</p>;
}
return (
<div>
<h1>My Todo List (Optimistic)</h1>
<ul>
{data?.map(todo => (
<li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.title}
</li>
))}
</ul>
</div>
);
}
function App() {
return (
<div style={{ fontFamily: 'sans-serif', padding: '20px' }}>
<TodoList />
<AddTodoForm />
</div>
);
}
export default App;
Explanation of new code for Optimistic Updates:
useMutation<Todo, Error, string, { previousTodos: Todo[] }>: Notice the fourth generic type parameter. This is theContexttype, which is an object that will be passed betweenonMutate,onError, andonSettled. We’re using it to store ourpreviousTodossnapshot.onMutate: async (newTodoTitle: string) => { ... }: This is the heart of the optimistic update.await queryClient.cancelQueries({ queryKey: ['todos'] });: It’s crucial to cancel any ongoing['todos']fetches. If a background refetch completes after our optimistic update but before the server confirms, it could overwrite our optimistic UI with stale data.const previousTodos = queryClient.getQueryData<Todo[]>(['todos']);: We grab the current state of the['todos']query from the cache before we modify it. This snapshot is our “undo” button.queryClient.setQueryData<Todo[]>(['todos'], (old) => [...(old || []), { id: 'optimistic-id-' + Date.now(), title: newTodoTitle, completed: false }]);: This is the optimistic part! We directly update the['todos']query in the cache. We add a new todo with a temporary, client-side-generated ID. This instantly updates the UI.return { previousTodos: previousTodos || [] };: We return thepreviousTodossnapshot as part of the context object.
onError: (err, newTodoTitle, context) => { ... }: IfmockApi.addTodofails (remember our20% chance of failureinmockApi.ts?), this callback runs.queryClient.setQueryData(['todos'], context?.previousTodos);: We use thepreviousTodossnapshot from thecontextto revert the cache to its state before the optimistic update. The temporary todo disappears, and the UI reflects the actual server state (which didn’t change).
onSettled: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }); ... }: This callback runs whether the mutation succeeded or failed. Its job is to ensure that, regardless of optimistic updates or rollbacks, the client eventually syncs with the actual server state.invalidateQuerieswill trigger a refetch of['todos'], which will replace any optimistic data (or rolled-back data) with the definitive server data.
Now, try adding todos again!
- Most of the time, the new todo will appear instantly in the list, then after a brief delay, the temporary ID will be replaced by the real server-generated ID (if you inspect the element or use the Devtools).
- Occasionally (about 20% of the time, due to our
mockApisetup), the mutation will fail. When it does, you’ll see the todo briefly appear, then disappear, and an alert will pop up. This demonstrates the rollback in action!
Congratulations! You’ve successfully implemented optimistic updates, a hallmark of highly performant and user-friendly web applications.
Mini-Challenge
Now it’s your turn to apply what you’ve learned!
Challenge: Implement a “Delete Todo” mutation with optimistic updates.
- Add a “Delete” button: To each todo item in the
TodoListcomponent. - Create a
useMutationhook: For deleting a todo, similar toaddTodoMutation.- Its
mutationFnshould callmockApi.deleteTodo(id).
- Its
- Implement Optimistic Updates for deletion:
- In
onMutate:- Cancel
['todos']queries. - Snapshot
previousTodos. - Optimistically remove the deleted todo from the cache using
queryClient.setQueryData.
- Cancel
- In
onError:- Rollback the cache using
context.previousTodos.
- Rollback the cache using
- In
onSettled:- Invalidate
['todos']to refetch the true server state.
- Invalidate
- In
Hint: When optimistically removing an item in onMutate, you’ll likely want to filter the previousTodos array to exclude the item being deleted. Remember to return the previousTodos in the context.
What to observe/learn:
- The delete action should feel instant.
- If the simulated deletion fails (due to
mockApi’s random error), the todo should reappear in the list, demonstrating the rollback. - Confirm that the
onSettledinvalidation correctly syncs the UI with the server after the mutation, whether it succeeded or failed.
Take your time, experiment, and don’t be afraid to consult the TanStack Query documentation for useMutation!
Common Pitfalls & Troubleshooting
Working with mutations and optimistic updates can sometimes lead to unexpected behavior. Here are a few common pitfalls and how to troubleshoot them:
- Forgetting
invalidateQueriesafter a non-optimistic mutation:- Symptom: You perform a mutation (e.g., add a todo), the server confirms it, but your list of todos on the UI doesn’t update.
- Reason: TanStack Query doesn’t know your server data has changed unless you tell it. Without
queryClient.invalidateQueries(), the cacheduseQuerydata remains “fresh” and won’t refetch. - Solution: Always include
queryClient.invalidateQueries({ queryKey: ['yourQueryKey'] })in youronSuccessoronSettledcallbacks for mutations that affect existing query data.
- Incorrect
queryKeyininvalidateQueries:- Symptom: You invalidate a query, but the wrong data (or no data) refetches.
- Reason: Query keys are fundamental. If your
invalidateQuerieskey doesn’t exactly match thequeryKeyused by youruseQueryhooks, it won’t work. Remember thatqueryKey: ['todos', { status: 'active' }]is different fromqueryKey: ['todos']. - Solution: Double-check that the
queryKeypassed toinvalidateQueriesprecisely matches thequeryKeyof the queries you intend to refetch.
- Complex Optimistic Rollbacks / State Management:
- Symptom: Optimistic updates work, but rollbacks are buggy, or the UI ends up in an inconsistent state after an error.
- Reason: Managing the snapshot and rollback logic can be tricky, especially with complex data structures or multiple interconnected queries. Missing data in the snapshot or incorrect
setQueryDatalogic can cause issues. - Solution:
- Ensure your
onMutatecallback captures a complete and accurate snapshot of the relevant query data usingqueryClient.getQueryData(). - Your
onErrorcallback should use this snapshot to fully revert the cache. - Always have
queryClient.invalidateQueriesinonSettledas a safety net to refetch the definitive server state. - Use
queryClient.cancelQueriesinonMutateto prevent race conditions. - The TanStack Query Devtools are invaluable here for observing cache changes.
- Ensure your
- Race Conditions with Optimistic Updates and Background Refetches:
- Symptom: An optimistic update happens, but then the UI briefly reverts to an older state before correcting itself.
- Reason: A background refetch (e.g., due to window focus, interval refetch, or another component’s
useQuery) might complete after youronMutateoptimistic update but before your server mutation resolves. This refetch overwrites your optimistic data with stale server data. - Solution: Use
await queryClient.cancelQueries({ queryKey: ['yourQueryKey'] })at the very beginning of youronMutatefunction. This stops any ongoing fetches for that query, allowing your optimistic update to take precedence.
Remember, the TanStack Query Devtools (ReactQueryDevtools) are your best friend when debugging these scenarios. They allow you to inspect the query cache, see when queries are fetching, stale, or inactive, and observe mutation lifecycles.
Summary
Phew, that was a lot of ground covered! You’ve just unlocked some of the most powerful features of TanStack Query (v5) for building highly dynamic and responsive applications. Let’s recap the key takeaways:
useMutation: This hook is your go-to for performing server-side data changes (create, update, delete). It provides state likeisPending,isError, andisSuccessto manage the mutation’s lifecycle.- Query Invalidation: After a successful mutation,
queryClient.invalidateQueries({ queryKey: ['yourKey'] })is essential. It tells TanStack Query that cached data for a specific key is stale, prompting a refetch and ensuring your UI reflects the latest server state. - Optimistic Updates: This advanced technique enhances user experience by updating the UI before the server confirms the mutation. It makes your app feel incredibly fast.
onMutate: Used to cancel ongoing queries, snapshot the current cache (for rollback), and optimistically update the cache.onError: Used to rollback the optimistic changes if the server mutation fails, using the snapshot taken inonMutate.onSettled: Always runs after a mutation (success or failure) to invalidate queries and ensure the UI eventually synchronizes with the definitive server state.
- Context for Rollback: The fourth generic type parameter in
useMutationallows you to pass a context object fromonMutatetoonErrorandonSettled, typically used to carry the snapshot for rollback. - Best Practices: Always cancel queries in
onMutate, take robust snapshots, and useonSettledto invalidate for eventual consistency.
You’re now equipped to handle complex data interactions with grace and performance. This mastery of mutations and optimistic updates is a significant step towards becoming a TanStack Query expert!
What’s Next?
In Chapter 6, we’ll shift gears from data fetching to data display and interaction by diving into TanStack Table. We’ll learn how to build powerful, customizable data grids that can handle large datasets efficiently, often integrating seamlessly with the data fetching capabilities of TanStack Query!
References
- TanStack Query v5 Docs: Mutations
- TanStack Query v5 Docs: Query Invalidation
- TanStack Query v5 Docs: Optimistic Updates
- TanStack Query v5 Docs:
useMutationAPI
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.