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 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
Section titled “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: QueryStatustype InfiniteQueryObserver<T, E> = Observer<InfiniteQueryResource<T, E>>; // Status: QueryStatustype MutationObserver<V, T, E> = Observer<MutationResource<V, T, E>>; // Status: MutationStatusEach 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
Section titled “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
Section titled “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 } }}ObserverConfig
Section titled “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>
Section titled “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
Section titled “How the hooks use it”| Hook | Observer | Status type |
|---|---|---|
use_query |
QueryObserver<T, E> |
QueryStatus |
use_infinite_query |
InfiniteQueryObserver<T, E> |
QueryStatus |
use_mutation |
MutationObserver<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
Section titled “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
Section titled “Next steps”- Queries / Infinite queries / Mutations: the hooks that build observers for you.
- DevTools: diagnostics read the same live entities observers watch.
- Error handling: why
Cancelledrarely reaches your UI (it is a status, deduplicated like any other).