Tanstack UseMutation Guide
ReactTanstack QueryJavaScriptState Management
- This one is a bit Long. But it clears the concept.
The useMutation
hook is a core feature of Tanstack Query that handles server-side effects like creating, updating, or deleting data.
What is useMutation?
useMutation
is a hook designed to handle:
- Data mutations (POST, PUT, DELETE operations)
- Loading and error states during mutations
- Success and error callbacks
- Optimistic updates
- Retry logic for failed mutations
- Rollback mechanisms
- Cache invalidation and updates
Unlike useQuery
which is for fetching data, useMutation
is specifically for operations that change data on the server.
Basic Syntax
const {
mutate,
mutateAsync,
isLoading,
isError,
isSuccess,
error,
data,
reset,
} = useMutation({
mutationFn: (variables) => {
return axios.post("/api/data", variables);
},
onSuccess: (data, variables, context) => {
// Handle success
},
onError: (error, variables, context) => {
// Handle error
},
onSettled: (data, error, variables, context) => {
// Called regardless of success or failure
},
});
Core Parameters
- mutationFn: The function that performs the mutation (required)
- onSuccess: Callback function called when mutation is successful
- onError: Callback function called when mutation fails
- onMutate: Callback function called before mutation is executed
- onSettled: Callback function called when mutation is either successful or has failed
- retry: Number of retry attempts for failed mutations (default: 0)
- retryDelay: Delay between retry attempts
- useErrorBoundary: Boolean to determine if errors should propagate to the nearest error boundary
Returned Properties and Methods
- mutate: Function to trigger the mutation (synchronous)
- mutateAsync: Function to trigger the mutation, returns a Promise
- isLoading/isPending: Boolean indicating if the mutation is in progress
- isError: Boolean indicating if the mutation resulted in an error
- isSuccess: Boolean indicating if the mutation was successful
- isIdle: Boolean indicating if the mutation is idle
- error: Error object if the mutation failed
- data: Data returned from a successful mutation
- reset: Function to reset the mutation state
When to Use useMutation
- Creating new resources (POST requests)
- Updating existing resources (PUT/PATCH requests)
- Deleting resources (DELETE requests)
- Any server-side operation that modifies data
- Form submissions
- User actions that require server-side changes
- When you need to handle loading/error states for data modifications
- When you need to update the UI optimistically
Basic Usage Example
function CreateTodo() {
const queryClient = useQueryClient();
const { mutate, isLoading, isError, error } = useMutation({
mutationFn: (newTodo) => {
return axios.post("/api/todos", newTodo);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});
const handleSubmit = (e) => {
e.preventDefault();
mutate({ title: "New Todo", completed: false });
};
return (
<div>
<button onClick={handleSubmit} disabled={isLoading}>
{isLoading ? "Adding..." : "Add Todo"}
</button>
{isError && <div>Error: {error.message}</div>}
</div>
);
}
Advanced Usage
Optimistic Updates
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
await queryClient.cancelQueries({ queryKey: ["todos"] });
const previousTodos = queryClient.getQueryData(["todos"]);
queryClient.setQueryData(["todos"], (old) => {
return old.map((todo) => {
if (todo.id === newTodo.id) {
return { ...todo, ...newTodo };
}
return todo;
});
});
return { previousTodos };
},
onError: (err, newTodo, context) => {
queryClient.setQueryData(["todos"], context.previousTodos);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});
Mutation Variables
const { mutate } = useMutation({
mutationFn: (newTodo) => {
return axios.post("/api/todos", newTodo);
},
});
mutate({ title: "Do laundry", completed: false });
Using mutateAsync for Promise Chains
const { mutateAsync } = useMutation({
mutationFn: createTodo,
});
const handleSubmit = async () => {
try {
const todo = await mutateAsync({ title: "New todo" });
console.log("New todo:", todo);
} catch (error) {
console.error("Failed to create todo:", error);
}
};
Reset Mutation State
const { mutate, isSuccess, reset } = useMutation({
mutationFn: createTodo,
});
if (isSuccess) {
resetForm();
reset();
}
Multiple Mutations
const createTodoMutation = useMutation({
mutationFn: createTodo,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});
const updateTodoMutation = useMutation({
mutationFn: updateTodo,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});
const deleteTodoMutation = useMutation({
mutationFn: deleteTodo,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});
Integration with React Forms
Basic Form Integration
function TodoForm() {
const [title, setTitle] = useState("");
const queryClient = useQueryClient();
const { mutate, isLoading } = useMutation({
mutationFn: (newTodo) => axios.post("/api/todos", newTodo),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["todos"] });
setTitle("");
},
});
const handleSubmit = (e) => {
e.preventDefault();
mutate({ title, completed: false });
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
disabled={isLoading}
/>
<button type="submit" disabled={isLoading}>
{isLoading ? "Adding..." : "Add Todo"}
</button>
</form>
);
}
With React Hook Form
import { useForm } from "react-hook-form";
function TodoFormWithHookForm() {
const { register, handleSubmit, reset } = useForm();
const queryClient = useQueryClient();
const { mutate, isLoading } = useMutation({
mutationFn: (data) => axios.post("/api/todos", data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["todos"] });
reset();
},
});
const onSubmit = (data) => {
mutate({ ...data, completed: false });
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("title")} disabled={isLoading} />
<button type="submit" disabled={isLoading}>
{isLoading ? "Adding..." : "Add Todo"}
</button>
</form>
);
}
Best Practices
- Use
queryClient
for cache updates - Implement optimistic updates for better UX
- Handle loading and error states
- Use
onSettled
for universal cleanup - Organize mutations in custom hooks
- Use retry cautiously
- Consider side effects carefully
- Use
onMutate
and context for rollback
Complete Example
function useTodoMutations() {
const queryClient = useQueryClient();
const create = useMutation({
mutationFn: (newTodo) => axios.post("/api/todos", newTodo),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});
const update = useMutation({
mutationFn: (updatedTodo) =>
axios.put(`/api/todos/${updatedTodo.id}`, updatedTodo),
onMutate: async (newTodo) => {
await queryClient.cancelQueries({ queryKey: ["todos"] });
const previousTodos = queryClient.getQueryData(["todos"]);
queryClient.setQueryData(["todos"], (old) =>
old.map((todo) => (todo.id === newTodo.id ? newTodo : todo))
);
return { previousTodos };
},
onError: (err, newTodo, context) => {
queryClient.setQueryData(["todos"], context.previousTodos);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});
const remove = useMutation({
mutationFn: (id) => axios.delete(`/api/todos/${id}`),
onMutate: async (id) => {
await queryClient.cancelQueries({ queryKey: ["todos"] });
const previousTodos = queryClient.getQueryData(["todos"]);
queryClient.setQueryData(["todos"], (old) =>
old.filter((todo) => todo.id !== id)
);
return { previousTodos };
},
onError: (err, id, context) => {
queryClient.setQueryData(["todos"], context.previousTodos);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});
return { create, update, remove };
}
function TodoApp() {
const { data: todos, isLoading } = useQuery({
queryKey: ["todos"],
queryFn: () => axios.get("/api/todos").then((res) => res.data),
});
const { create, update, remove } = useTodoMutations();
const [newTodoTitle, setNewTodoTitle] = useState("");
const handleCreateTodo = (e) => {
e.preventDefault();
create.mutate({ title: newTodoTitle, completed: false });
setNewTodoTitle("");
};
const handleToggleTodo = (todo) => {
update.mutate({ ...todo, completed: !todo.completed });
};
const handleDeleteTodo = (id) => {
remove.mutate(id);
};
if (isLoading) return <div>Loading todos...</div>;
return (
<div>
<form onSubmit={handleCreateTodo}>
<input
value={newTodoTitle}
onChange={(e) => setNewTodoTitle(e.target.value)}
disabled={create.isLoading}
/>
<button type="submit" disabled={create.isLoading}>
{create.isLoading ? "Adding..." : "Add Todo"}
</button>
</form>
{create.isError && <div>Error creating todo: {create.error.message}</div>}
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggleTodo(todo)}
disabled={update.isLoading}
/>
<span
style={{
textDecoration: todo.completed ? "line-through" : "none",
}}
>
{todo.title}
</span>
<button
onClick={() => handleDeleteTodo(todo.id)}
disabled={remove.isLoading}
>
Delete
</button>
</li>
))}
</ul>
</div>
);
}
You made it!
This comprehensive guide covers everything you need to know about the
useMutation
hook in Tanstack Query, including its purpose, syntax, usage
patterns, and best practices.
The Goodbye 👋
Hope this cleared things up! If you liked the article or have questions, drop a message on my socials.
Thanks for reading!