# UseMutation
*The `UseMutation` [hook](../01_RulesOfHooks.md) provides a way to control [query](./09_UseQuery.md) caches from different components, enabling optimistic updates, cache invalidation, and cross-component data synchronization.*
## Overview
`UseMutation` enables you to control query caches irrespective of where they are used. It supports:
- **Optimistic Updates**: Update cache immediately before server confirmation.
- **Cross-Component Control**: Trigger updates from components that don't consume the data.
- **Background Revalidation**: Refresh data without clearing the current cache.
## Basic Usage
Update the UI immediately while the server processes the request.
```csharp
public class LikeButton : ViewBase
{
public record Post(int Likes, bool IsLiked);
public override object? Build()
{
var mutator = UseMutation<Post, string>("post-123");
var query = UseQuery("post-123", _ => Task.FromResult(new Post(10, false)));
var current = query.Value ?? new Post(0, false);
return new Button($"Like ({current.Likes})", _ =>
{
if (query.Value is not {} p) return;
mutator.Mutate(p with {
Likes = p.IsLiked ? p.Likes - 1 : p.Likes + 1,
IsLiked = !p.IsLiked
}, revalidate: false);
}).Variant(current.IsLiked ? ButtonVariant.Primary : ButtonVariant.Outline);
}
}
```
## Mutation Flow
```mermaid
sequenceDiagram
participant C as Component
participant M as UseMutation
participant Q as Query Cache
participant S as Server
Note over C,S: Optimistic Update
C->>M: Mutate(newValue)
M->>Q: Update cache immediately
Q-->>C: UI updates instantly
M->>S: Revalidate in background
S-->>Q: Return confirmed data
Q-->>C: UI updates with server data
```
## Methods
The hook returns a `QueryMutator` object. Use the typed generic version for optimistic updates.
```csharp
// Typed (Recommended for optimistic updates)
var mutator = UseMutation<User, string>("user-profile");
// Untyped (Good for simple invalidation)
var mutator = UseMutation("user-profile");
```
| Method | Description | Usage |
|--------|-------------|-------|
| `Mutate(value, revalidate)` | Updates cache immediately with `value`. If `revalidate` is true, triggers a background fetch after. | Optimistic UI updates (e.g., Like button). |
| `Revalidate()` | Triggers a background refresh. Keeps showing stale data until new data arrives. | Non-destructive updates (e.g., Edit form save). |
| `Invalidate()` | Clears the cache and forces a refetch. UI enters "switching" or "loading" state. | Destructive operations (e.g., Delete item). |
## Query Scopes
`UseMutation` supports the same scopes as `UseQuery`, **except `View` scope**.
| Scope | Support | Reason |
|-------|---------|--------|
| `Server`, `App`, `Device` | ✅ Supported | Shared state can be accessed by key. |
| `View` | ❌ Not Supported | View-scoped queries are isolated to a specific component instance and cannot be targeted externally. |
## Best Practices & Troubleshooting
* **Keys Must Match Exactly**: "user-data" and "User-Data" are different keys.
* **Use Typed Mutations**: You cannot call `Mutate(value)` on an untyped `UseMutation("key")`. You must provide types: `UseMutation<T, TKey>("key")`.
* **Revalidate vs Invalidate**:
* Use **Revalidate** when you want to keep showing the current data while updating (e.g., "Refresh" button).
* Use **Invalidate** when the current data is definitely wrong or deleted (e.g., "Delete" button).
> **Warning:** If your mutation isn't working, check if the target `UseQuery` is using <code>Scope = QueryScope.View</code>. UseMutation cannot see View-scoped queries.
## See Also
- [UseQuery](./09_UseQuery.md)
- [Rules of Hooks](../02_RulesOfHooks.md)
## Examples
### Form Submission
Update data locally then sync with server.
```csharp
public class UserForm : ViewBase
{
public record User(string Name);
public override object? Build()
{
var name = UseState("");
var mutator = UseMutation<User, string>("user-profile");
var query = UseQuery("user-profile", async ct =>
{
await Task.Delay(100);
return new User("Guest");
});
return Layout.Vertical(
Text.Literal($"Current Profile: {query.Value?.Name ?? "Loading..."}"),
Layout.Horizontal(
name.ToTextInput("Enter Name"),
new Button("Save", async _ =>
{
if (string.IsNullOrEmpty(name.Value)) return;
mutator.Mutate(new User(name.Value), revalidate: false);
name.Set("");
await Task.CompletedTask;
})
)
);
}
}
```
### Shared Control (Cross-Component)
Control a query from a completely separate component (e.g., a header button controlling a list).
```csharp
public class SharedControlDemo : ViewBase
{
public override object? Build()
{
return Layout.Vertical(
new RefreshHeader(),
new Separator(),
new StatsDisplay()
);
}
}
public class RefreshHeader : ViewBase
{
public override object? Build()
{
var mutator = UseMutation("dashboard-stats");
return Layout.Horizontal(
new Button("Refresh (Revalidate)", _ => mutator.Revalidate()),
new Button("Force Reload (Invalidate)", _ => mutator.Invalidate())
);
}
}
public class StatsDisplay : ViewBase
{
public override object? Build()
{
var query = UseQuery("dashboard-stats", async ct =>
{
await Task.Delay(1000);
return $"Stats Updated: {DateTime.Now:HH:mm:ss}";
});
if (query.Loading) return Text.Literal("Loading new stats...");
return Layout.Vertical(
Text.H4("Dashboard Stats"),
Text.Literal(query.Value ?? ""),
query.Validating ? Text.Muted("Refreshing in background...") : null
);
}
}
```