Skip to main content

Observers

Observers bridge gpui-query's resources to GPUI's reactivity. An observer attaches to a resource entity and calls cx.notify() to trigger a re-render — but only when the resource's status actually changes. This is the primitive that keeps use_query, use_infinite_query, and use_mutation from re-rendering your view on every internal state mutation.

The Observer type

There is a single generic implementation, Observer<R>, parameterized over the resource kind. Three type aliases name the concrete observers you will use:

// All three are Observer<R> with a different R / Status pair.
use gpui_query::client::{
QueryObserver, InfiniteQueryObserver, MutationObserver,
};

type QueryObserver<T, E> = Observer<QueryResource<T, E>>; // Status: QueryStatus
type InfiniteQueryObserver<T, E> = Observer<InfiniteQueryResource<T, E>>; // Status: QueryStatus
type MutationObserver<V, T, E> = Observer<MutationResource<V, T, E>>; // Status: MutationStatus

Each resource exposes its status via an inherent status() method; the ObservableResource trait (pub-crate) surfaces it generically so Observer<R> can deduplicate notifications for any resource kind.

Status deduplication

This is the whole point of the observer. Without it, a raw cx.observe would call cx.notify() on every entity mutation — including intermediate ones like increment_retry() that leave the status at Loading. A query retrying three times would re-render the view two or three extra times for nothing.

Observer tracks the last status it saw and notifies only on change:

// (sketch of the dedup logic inside Observer::observe)
let last_status: Cell<Option<R::Status>> = Cell::new(None);

cx.observe(&upgraded, move |_, entity, cx| {
let current = entity.read(cx).observable_status();
if notify_on_change {
if last_status.get() != Some(current) {
last_status.set(Some(current));
cx.notify(); // status actually changed
}
} else {
cx.notify(); // unconditional mode
}
});

The result: increment_retry() / prepare_retry() calls (status stays Loading) no longer trigger re-renders. Your view re-renders on the transitions that matter — Idle → LoadingEmpty → Success, Success → LoadingWithData → Success, Loading → Failure, and so on.

Creating and observing

Build an observer from an entity, then call observe(cx) to start watching. observe returns Option<Subscription>None if the entity was already dropped. Store the returned subscription; dropping it ends the observation.

use gpui_query::client::QueryObserver;
# use gpui_query::QueryResource;
# #[derive(Clone)] struct T;
# #[derive(Clone, Debug)] struct E;

struct MyView {
users: gpui::Entity<QueryResource<T, E>>,
_subscription: gpui::Subscription,
}

impl MyView {
fn new(users: gpui::Entity<QueryResource<T, E>>, cx: &mut gpui::Context<Self>) -> Self {
let observer = QueryObserver::<T, E>::new(&users);
let subscription = observer.observe(cx)
.expect("entity was just created; cannot be dropped");
Self { users, _subscription: subscription }
}
}
note

The hooks (use_query, use_infinite_query, use_mutation) already construct the observer and return the subscription for you. You only need to create an observer yourself when you are subscribing to a resource you obtained another way — for example, one you pulled out of the QueryClient with query::<T, E>(key).

ObserverConfig

ObserverConfig::notify_on_status_change_only controls the dedup behavior. It defaults to true, which is what you almost always want. Set it to false to get unconditional notifications on every entity mutation:

use gpui_query::client::{Observer, ObserverConfig};
# use gpui_query::QueryResource;
# #[derive(Clone)] struct T;
# #[derive(Clone, Debug)] struct E;
# fn doc(entity: gpui::Entity<QueryResource<T, E>>) {
# let cx: &mut gpui::Context<()> = unimplemented!();

let sub = Observer::new(&entity)
.with_config(ObserverConfig { notify_on_status_change_only: false })
.observe(cx);
# }

Use unconditional mode only when you genuinely need to react to non-status fields (a custom retry counter you display inline, for example). It is strictly more re-renders.

Option<Subscription>

observe() returns Option<Subscription> rather than panicking. In debug builds the hooks panic! if this is None right after creating an entity (it would indicate a GPUI internal regression); in release builds they fall back to a no-op subscription so a misbehaving observer cannot crash the app. When you construct an observer yourself, decide explicitly: .expect(...) if a None is impossible in your context, or match / let else to handle it gracefully.

How the hooks use it

HookObserverStatus type
use_queryQueryObserver<T, E>QueryStatus
use_infinite_queryInfiniteQueryObserver<T, E>QueryStatus
use_mutationMutationObserver<V, T, E>MutationStatus

Each hook creates its observer, calls observe(cx), and returns the subscription as the second element of its tuple. The status-dedup is what makes a retrying mutation or a refetching query cheap to render — only terminal status changes reach your view.

ObservableResource

ObservableResource is the pub(crate) trait that lets Observer<R> read a status generically. It is not part of the public API, but it is why the three resource kinds share one observer implementation: each resource exposes status() with its own associated Status type, and Observer deduplicates on that. If you write your own resource type that should be observable, it will need the same status() method shape — but for application code, the three built-in observers cover every resource the hooks produce.

Next steps