Skip to main content

Mutations

Mutations handle write operations: creating a user, updating a record, deleting a resource. Unlike Queries, mutations don't run automatically. You trigger them explicitly, and they manage their own loading/success/failure lifecycle.

A mutation does not touch the query cache on its own. If you need to invalidate or refetch queries after a mutation succeeds, do that in an on_success callback. This is by design: the library doesn't guess which queries a mutation should affect.

use_mutation

pub fn use_mutation<V, T, E, C>(cx: &mut Context<C>) -> Entity<MutationResource<V, T, E>>

Creates a new MutationResource entity and returns it. The entity starts in the Idle state with a no-retry policy. Store it on your view struct and read from it during render to show loading spinners, error messages, or success state.

use gpui_query::{use_mutation, MutationResource};

struct UserForm {
create_user: Entity<MutationResource<NewUser, User>>,
}

impl UserForm {
fn new(cx: &mut Context<Self>) -> Self {
Self {
create_user: use_mutation(cx),
}
}
}

The returned entity is a GPUI Entity, so you observe it like any other entity: call entity.read(cx) to inspect status, data, and error fields during render.

use_mutation_with_options

pub fn use_mutation_with_options<V, T, E, C>(
options: &MutationOptions<V, T, E>,
cx: &mut Context<C>,
) -> Entity<MutationResource<V, T, E>>

Same as use_mutation but accepts a MutationOptions to configure retry policy and garbage collection time. See the MutationOptions section below.

mutate

pub fn mutate<V, T, E, C, F, Fut>(
entity: &Entity<MutationResource<V, T, E>>,
variables: V,
mutator: F,
cx: &mut Context<C>,
)

Triggers a mutation on the entity. It transitions the resource to Loading, spawns an async task running your mutator closure, and handles the outcome (including retries if the entity has a retry policy).

The mutator closure receives the variables and must return a Future<Output = Result<T, E>>. Each call to mutate overwrites any previous state on the entity: variables, error, and data are reset.

use gpui_query::{use_mutation, mutate};

mutate(&self.create_user, NewUser { name: "Alice" }, |vars| async move {
let resp = reqwest::post("/api/users")
.json(&vars)
.send()
.await?;
let user: User = resp.json().await?;
Ok::<User, MyError>(user)
}, cx);

mutate holds a WeakEntity reference internally, so dropping the entity before the async task finishes is safe. The task simply exits.

Concurrency

There is no built-in guard against calling mutate while a previous mutation on the same entity is still in flight. The second call overwrites the entity's state immediately. If you need to prevent concurrent mutations, check entity.read(cx).is_loading() before calling mutate.

mutate_with_callbacks

pub fn mutate_with_callbacks<V, T, E, C, F, Fut>(
entity: &Entity<MutationResource<V, T, E>>,
variables: V,
mutator: F,
callbacks: MutationCallbacks<T, E>,
cx: &mut Context<C>,
)

Identical to mutate but accepts a MutationCallbacks struct that fires on the final outcome. Callbacks run after all retries are exhausted (on failure) or on first success. They do not fire on intermediate retry attempts.

This is where you invalidate query caches after a successful write:

use gpui_query::{mutate_with_callbacks, MutationCallbacks};

mutate_with_callbacks(
&self.create_user,
NewUser { name: "Alice" },
|vars| async move {
Ok::<User, MyError>(create_user(vars).await)
},
MutationCallbacks::new()
.on_success(|data| {
// invalidate user list queries so they refetch
println!("created user: {:?}", data);
})
.on_error(|err| {
eprintln!("mutation failed: {:?}", err);
}),
cx,
);

See the MutationCallbacks section for the full callback API.

use_mutation_state

pub fn use_mutation_state<V, T, E, C>(cx: &mut Context<C>) -> Vec<Entity<MutationResource<V, T, E>>>

Returns a snapshot of all MutationResource entities matching the (V, T, E) type triple registered in the global QueryClient. Useful for devtools or status bars that need to observe all mutations of a particular kind across the entire app.

Returns an empty Vec if no mutations exist for that type triple or if no QueryClient is set up.

use gpui_query::use_mutation_state;

let active_mutations = use_mutation_state::<NewUser, User, MyError, _>(cx);
for entity in &active_mutations {
let m = entity.read(cx);
if m.is_loading() {
// show a global spinner
}
}

MutationResource<V, T, E>

The core state container for a single mutation. Three type parameters:

  • V is the variables (input) type. What you pass into the mutation.
  • T is the success output type. The data returned on completion.
  • E is the error type. Defaults to QueryError.

Lifecycle

Every mutation goes through these states in order:

  1. Idle: initial state. No mutation has been triggered.
  2. Loading: begin(variables, now_ms) was called. The variables are stored, the error is cleared, and a fresh cancellation signal is created.
  3. Success or Failure: the async task completes. complete_success(data) stores the result and clears the error. complete_failure(error) stores the error and increments the retry counter.

If the entity has a retry policy and retries remain after a failure, the internal loop calls retry() to transition back to Loading and runs the mutator again.

Accessors

MethodReturn typeDescription
status()MutationStatusCurrent status enum
data()Option<&T>Most recent success data
error()Option<&E>Most recent error
variables()Option<&V>Current or most recent variables
retry_count()u32How many retries have been attempted
retry_policy()&RetryPolicyThe retry policy for this resource
created_at()u64Timestamp (ms) of the last begin call, or 0
signal()Option<&QuerySignal>The cancellation signal, if the mutation is in flight
key()Option<&QueryKey>Optional query key for cache correlation

Boolean helpers

is_idle(), is_loading(), is_success(), is_failure() each return bool. These are convenience wrappers around status() checks.

Methods

begin(variables, now_ms) transitions to Loading. Stores the variables, clears any previous error, records the timestamp, and creates a fresh QuerySignal.

complete_success(data) transitions to Success. Stores the result data and clears the signal.

complete_failure(error) transitions to Failure. Stores the error, increments retry_count, and clears the signal.

should_retry() -> bool checks whether retry_count < max_retries according to the retry policy.

retry() -> bool attempts to transition from Failure back to Loading. Only succeeds when should_retry() is true and the current status is Failure. Preserves the original variables and creates a fresh signal. Returns true if the retry was initiated.

cancel(error) forces the mutation into Failure with the given error. Cancels the signal so any in-flight async work can observe it.

reset() returns the resource to Idle and clears everything: data, error, variables, retry count, signal, and timestamp.

with_key(key) associate a QueryKey with this mutation for cache correlation. Returns self for builder-style chaining.

MutationStatus

pub enum MutationStatus {
Idle,
Loading,
Success,
Failure,
}

Each variant has a label() method returning a &'static str ("Idle", "Loading", "Success", "Failure").

MutationOptions<V, T, E>

Configuration struct passed to use_mutation_with_options.

FieldTypeDefaultDescription
retry_policyRetryPolicyRetryPolicy::no_retries()Controls automatic retry on failure
gc_time_msu64300_000 (5 minutes)How long idle mutation resources survive before garbage collection

Builder methods:

use gpui_query::{MutationOptions, RetryPolicy};

let opts: MutationOptions<NewUser, User> = MutationOptions::new()
.retry_policy(RetryPolicy::new(3).with_exponential_backoff())
.gc_time_ms(10 * 60 * 1_000); // 10 minutes

MutationCallbacks<T, E>

A struct of optional closures that fire when a mutation settles. Construct it with the builder pattern:

use gpui_query::MutationCallbacks;

let callbacks = MutationCallbacks::new()
.on_success(|data: &User| {
// called on successful completion
})
.on_error(|err: &MyError| {
// called after all retries exhausted
})
.on_settled(|data: Option<&User>, err: Option<&MyError>| {
// always called, regardless of outcome
});

Each field is an Option<Box<dyn Fn(&T) + 'static>> (or the equivalent for the error and settled variants).

  • on_success receives &T. Fires once on first success.
  • on_error receives &E. Fires once after all retries are exhausted.
  • on_settled receives Option<&T> and Option<&E>. Exactly one will be Some. Fires after either on_success or on_error.

Callbacks fire on the final outcome only. Intermediate retry attempts do not trigger callbacks.

RetryPolicy

Shared between queries and mutations. See the Queries page for the full reference. The defaults for mutations differ: use_mutation creates a resource with RetryPolicy::no_retries(), while MutationOptions::new() also defaults to no retries.

To add retries:

use gpui_query::RetryPolicy;

// Fixed 1-second delay, up to 3 retries
let policy = RetryPolicy::new(3);

// Exponential backoff: 1s, 2s, 4s, capped at 30s
let policy = RetryPolicy::new(3)
.with_delay(1000)
.with_exponential_backoff()
.with_max_delay(30_000);

Putting it together

A complete example of a view that creates a user and invalidates the user list on success:

use gpui::{Context, Entity, View};
use gpui_query::{
use_mutation, mutate_with_callbacks,
MutationCallbacks, MutationResource,
};

struct UserForm {
create_user: Entity<MutationResource<NewUser, User>>,
}

impl UserForm {
fn new(cx: &mut Context<Self>) -> Self {
Self {
create_user: use_mutation(cx),
}
}

fn handle_submit(&mut self, name: String, cx: &mut Context<Self>) {
mutate_with_callbacks(
&self.create_user,
NewUser { name },
|vars| async move {
Ok::<User, MyError>(api::create_user(&vars).await)
},
MutationCallbacks::new()
.on_success(|_user| {
// Trigger a refetch of the user list query.
// This is where you'd call invalidate_queries or
// refetch on the relevant query entity.
})
.on_error(|err| {
eprintln!("create user failed: {:?}", err);
}),
cx,
);
}
}

The mutation entity lives on the view struct. During render, read self.create_user.read(cx).status() to decide what to show. When the user submits the form, handle_submit fires the mutation. On success, the callback invalidates whatever queries need refreshing. The query cache is otherwise untouched.