Mutations use_mutation API reference for gpui-query: dispatching writes from GPUI components, success and error callbacks, and coordinating cache invalidation. Mutations handle write operations: creating a user, updating a record, deleting a resource. Unlike Queries (/docs/api/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(cx: &mut Context) -> Entity> 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>, } impl UserForm { fn new(cx: &mut Context) -> 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( options: &MutationOptions, cx: &mut Context, ) -> Entity> 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( entity: &Entity>, variables: V, mutator: F, cx: &mut Context, ) 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>. 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) }, 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( entity: &Entity>, variables: V, mutator: F, callbacks: MutationCallbacks, cx: &mut Context, ) 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::(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(cx: &mut Context) -> Vec>> 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::(cx); for entity in &active_mutations { let m = entity.read(cx); if m.is_loading() { // show a global spinner } } MutationResource 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 Method Return type Description status() MutationStatus Current 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() u32 How many retries have been attempted retry_policy() &RetryPolicy The retry policy for this resource created_at() u64 Timestamp (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 Configuration struct passed to use_mutation_with_options. Field Type Default Description retry_policy RetryPolicy RetryPolicy::no_retries() Controls automatic retry on failure gc_time_ms u64 300_000 (5 minutes) How long idle mutation resources survive before garbage collection Builder methods: use gpui_query::{MutationOptions, RetryPolicy}; let opts: MutationOptions = MutationOptions::new() .retry_policy(RetryPolicy::new(3).with_exponential_backoff()) .gc_time_ms(10 * 60 * 1_000); // 10 minutes MutationCallbacks 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> (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 (/docs/api/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>, } impl UserForm { fn new(cx: &mut Context) -> Self { Self { create_user: use_mutation(cx), } } fn handle_submit(&mut self, name: String, cx: &mut Context) { mutate_with_callbacks( &self.create_user, NewUser { name }, |vars| async move { Ok::(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.