Guides
Mutations
Mutations
Unlike queries, mutations are typically used to create/update/delete data or perform server side-effects. For this purpose, SwrSharp provides a UseMutation class.
Basic Mutation
var mutation = new UseMutation<Todo, CreateTodoInput>(
new MutationOptions<Todo, CreateTodoInput>
{
MutationFn = async input =>
{
var response = await httpClient.PostAsJsonAsync("/api/todos", input);
return await response.Content.ReadFromJsonAsync<Todo>();
}
},
queryClient
);
// Fire-and-forget
mutation.Mutate(new CreateTodoInput { Title = "New Todo" });
// Or await the result
var newTodo = await mutation.MutateAsync(new CreateTodoInput { Title = "New Todo" });
Mutation States
A mutation can only be in one of the following states at any given moment:
IsIdle- The mutation is idle or in a fresh/reset stateIsPending- The mutation is currently runningIsError- The mutation encountered an errorIsSuccess- The mutation was successful and mutation data is available
Beyond these primary states, more information is available depending on the state of the mutation:
Error- If the mutation is in an error state, the error is available via theErrorpropertyData- If the mutation is in a success state, the data is available via theDataproperty
var mutation = new UseMutation<Todo, CreateTodoInput>(
new MutationOptions<Todo, CreateTodoInput>
{
MutationFn = async input => await CreateTodo(input)
},
queryClient
);
if (mutation.IsPending)
{
// Show loading spinner
}
else if (mutation.IsError)
{
Console.WriteLine($"Error: {mutation.Error!.Message}");
}
else if (mutation.IsSuccess)
{
Console.WriteLine($"Created: {mutation.Data!.Title}");
}
Resetting Mutation State
Sometimes you need to clear the error or data of a mutation request. To do this, you can use the Reset method:
mutation.Reset();
// mutation.Status is now Idle
// mutation.Data is null
// mutation.Error is null
Mutation Side Effects
UseMutation comes with powerful helper callbacks that allow quick and easy side-effects at any stage during the mutation lifecycle.
var mutation = new UseMutation<Todo, CreateTodoInput>(
new MutationOptions<Todo, CreateTodoInput>
{
MutationFn = async input => await CreateTodo(input),
OnMutate = async (variables, context) =>
{
// Called before the mutation function fires
// Return value is passed to other callbacks as onMutateResult
Console.WriteLine("About to create todo...");
return null;
},
OnSuccess = async (data, variables, onMutateResult, context) =>
{
Console.WriteLine($"Todo created: {data.Title}");
// Invalidate and refetch related queries
context.Client.InvalidateQueries(new QueryFilters
{
QueryKey = new QueryKey("todos")
});
},
OnError = async (error, variables, onMutateResult, context) =>
{
Console.WriteLine($"Error creating todo: {error.Message}");
},
OnSettled = async (data, error, variables, onMutateResult, context) =>
{
// Called on both success and error
Console.WriteLine("Mutation finished");
}
},
queryClient
);
All callbacks receive a MutationContext that gives you access to the QueryClient, making it easy to perform cache operations like invalidation directly from your mutation callbacks.
Per-Call Callbacks
In addition to the callbacks defined on MutationOptions, you can also pass callbacks when calling Mutate() or MutateAsync(). These per-call callbacks only fire for the last Mutate() call when multiple calls happen in quick succession.
mutation.Mutate(
new CreateTodoInput { Title = "New Todo" },
new MutateOptions<Todo, CreateTodoInput>
{
OnSuccess = async (data, variables, onMutateResult, context) =>
{
// This only fires if this is the most recent Mutate() call
Console.WriteLine("This specific mutation succeeded!");
}
}
);
Callback Execution Order
When both option-level and per-call callbacks are defined:
OnMutate(option-level) - fires first- Mutation function executes
OnSuccess/OnError(option-level) - fires firstOnSuccess/OnError(per-call) - fires secondOnSettled(option-level) - fires firstOnSettled(per-call) - fires second
Important: Option-level callbacks fire for every mutation call. Per-call callbacks only fire for the last call.
Consecutive Mutations
When Mutate() is called multiple times, each call starts a new mutation. The Variables, Data, Error, and Status properties always reflect the most recent call.
mutation.Mutate(input1); // Starts mutation 1
mutation.Mutate(input2); // Starts mutation 2 (mutation 1 still runs)
// mutation.Variables is now input2
// Option-level callbacks fire for BOTH mutations
// Per-call callbacks only fire for mutation 2 (the last one)
MutateAsync
While Mutate() is fire-and-forget, MutateAsync() returns a Task that you can await:
try
{
var data = await mutation.MutateAsync(input);
Console.WriteLine($"Success: {data}");
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
Retry
By default, mutations do not retry on failure (unlike queries which retry 3 times). You can configure retry behavior:
var mutation = new UseMutation<Todo, CreateTodoInput>(
new MutationOptions<Todo, CreateTodoInput>
{
MutationFn = async input => await CreateTodo(input),
Retry = 3, // Retry up to 3 times
RetryDelay = TimeSpan.FromMilliseconds(1000), // Fixed delay
// Or use exponential backoff:
// RetryDelayFunc = attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)),
MaxRetryDelay = TimeSpan.FromSeconds(30)
},
queryClient
);
During retries, FailureCount and FailureReason track the current retry state.
Mutation Scopes
Mutations with the same scope run serially. This is useful when you need to ensure mutations are processed in order:
var mutation = new UseMutation<Todo, CreateTodoInput>(
new MutationOptions<Todo, CreateTodoInput>
{
MutationFn = async input => await CreateTodo(input),
Scope = new MutationScope { Id = "todos" }
},
queryClient
);
// These will run one after the other, not in parallel
mutation.Mutate(input1);
mutation.Mutate(input2);
mutation.Mutate(input3);