Cache, fetch,
render. One path.
gpui-query wires network, cache, retry, and revalidation into one small API for GPUI applications.
observer attached · watching "users"
// capabilities
Everything the fetch deserves
The full TanStack Query feature set, rebuilt around GPUI's entity and async model.
Declarative Queries
One hook. gpui-query handles fetching, caching, and state updates so your render code stays a pure function of data.
Smart Caching
Cache policies for every use case — pick one per query and move on.
Mutations
First-class mutation support with success and error callbacks, plus optimistic updates that roll back on failure.
Infinite Queries
Paginate effortlessly with built-in infinite query support and bidirectional fetching.
Cancellation
Signal-checked retries and cooperative cancellation for a clean async lifecycle — no orphaned tasks.
Persistence
Serialize and restore query state across launches with custom persistence backends.
QueryClient, live
Entries age, go stale, revalidate, and log each transition. Use the row controls to force the lifecycle.
event log
- T+000.0 observers registered · 3
- T+000.0 QueryClient::new() · gc 300s · policy swr
// surface area
Three hooks. That's the API.
Queries, mutations, and pagination share the same shape: a key, an async function, and your context. Everything returns an Entity you read in render.
let (users, _sub) = use_query( QueryOptions::new("users") .cache_policy(CachePolicy::StaleWhileRevalidate { ttl_ms: 60_000, stale_ms: 300_000, }) .retry_policy(RetryPolicy::new(3).with_exponential_backoff()), |signal| async move { fetch_users(&signal).await }, cx,);let (create, _sub) = use_mutation((), cx);mutate_with_callbacks( &create, NewUser { name: "Alice" }, |vars| async move { create_user(vars).await }, MutationCallbacks::new() .on_success(|_| { /* invalidate "users" */ }) .on_error(|err| eprintln!("failed: {err:?}")), cx,);let (feed, _sub) = use_infinite_query( InfiniteQueryOptions::new(QueryKey::from(["feed"])) .max_pages(Some(10)), |last_page| async move { let cursor = last_page.map(|p| p.cursor()); let page = fetch_page(cursor).await?; Ok((page.items, page.has_more)) }, cx,);// diff
The code you stop writing
The same feature — a fetched, cached user list — written both ways. Toggle to compare.
1struct UserList {2 users: Option<Vec<User>>,3 error: Option<String>,4 loading: bool,5 generation: u64,6}78impl UserList {9 fn fetch(&mut self, cx: &mut Context<Self>) {10 self.loading = true;11 self.generation += 1;12 let generation = self.generation;13 cx.spawn(async move |this, cx| {14 let result = fetch_users().await;15 this.update(cx, |this, cx| {16 if this.generation != generation {17 return; // superseded by a newer request18 }19 this.loading = false;20 match result {21 Ok(users) => this.users = Some(users),22 Err(err) => this.error = Some(err.to_string()),23 }24 cx.notify();25 })26 })27 .detach();28 }29}3031// still missing: caching, ttl, retry, dedup across views…1let (users, _sub) = use_query(2 "users",3 |signal| async move { fetch_users(&signal).await },4 cx,5);67// cached · retried · deduped · revalidated · cancellable▸ 31 lines of lifecycle plumbing → 7 lines, with more behavior.
// comparison
Why gpui-query?
See how gpui-query compares to hand-rolling async state in GPUI.
| Feature | gpui-query | cx.spawn() | Raw Futures |
|---|---|---|---|
| Caching | |||
| Auto Retry | |||
| Deduplication | |||
| Cache Policies | |||
| DevTools | |||
| Persistence | |||
| Type Safety | |||
| No Setup Required | |||
| Zero Dependencies |
FIG. 1 — anatomy of a query · hover a callout
Every line, accounted for
let (users, _sub) = use_query( QueryOptions::new("users")⟨A⟩ .cache_policy(CachePolicy::StaleWhileRevalidate {⟨B⟩ ttl_ms: 60_000, stale_ms: 300_000, }) .retry_policy(RetryPolicy::new(3)⟨C⟩ .with_exponential_backoff()), |signal| async move {⟨D⟩ fetch_users(&signal).await }, cx,⟨E⟩);// architecture
Three layers, one direction
Each layer has a single responsibility. Data flows down, results flow back up.
Hook Layer
use_query / use_mutation / use_infinite_query
Client Layer
QueryClient / Registry / GC
Core Layer
QueryResource / CachePolicy / QueryKey
Ship the lifecycle once
Add the crate, keep your views small, and let QueryClient own the async state machine.