Queries use_query API reference for gpui-query: query keys, fetchers, cache and request policies, status handling, and reactive updates in GPUI components. Queries are the core primitive. You define a key, a fetcher, and some options. The library handles caching, deduplication, retry, and reactive updates. Every query produces an Entity> that you read during render. When the entity's state changes, your component re-renders. use_query fn use_query( options: impl Into, fetcher: F, cx: &mut Context, ) -> (Entity>, Subscription) where F: Fn(QuerySignal) -> Fut + Send + 'static, Fut: Future> + Send + 'static, This is the primary hook. Call it in your component's constructor, not in render. It gets or creates a QueryResource entity from the global QueryClient, sets up an observer so your component re-renders on state changes, and spawns an async fetch if the resource is idle. The fetcher receives a QuerySignal for cooperative cancellation. Check signal.is_cancelled() periodically in long-running fetchers and abort early when possible. Returns a tuple of (Entity>, Subscription). Store the entity to read state during render. Store the subscription to keep the observation alive. Drop the subscription and the observation stops. use gpui_query::{use_query, QueryOptions, CachePolicy, RetryPolicy}; struct UserList { users: Entity, MyError>>, _sub: Subscription, } impl UserList { fn new(cx: &mut Context) -> Self { // Simple: just a string key let (users, _sub) = use_query("users", |signal| async move { let resp = reqwest::get("/api/users").await?; let users: Vec = resp.json().await?; Ok(users) }, cx); Self { users, _sub } } } With options: let (users, _sub) = use_query( QueryOptions::new("users") .cache_policy(CachePolicy::Ttl { ttl_ms: 300_000 }) .retry_policy(RetryPolicy::new(5).with_exponential_backoff()), |signal| async move { let resp = reqwest::get("/api/users").await?; Ok::<_, MyError>(resp.json().await?) }, cx, ); You can pass a &str, String, or QueryKey directly as the first argument because QueryOptions implements From for all three. The QueryClient must be initialized before calling use_query. In debug builds, the hook panics if cx.set_global::() was never called. In release builds, it falls back to a standalone entity with no shared caching or garbage collection. use_query_unsignalled fn use_query_unsignalled( key: QueryKey, cache_policy: CachePolicy, request_policy: RequestPolicy, fetcher: F, cx: &mut Context, ) -> (Entity>, Subscription) where F: Fn() -> Fut + Send + 'static, Fut: Future> + Send + 'static, The backward-compatible variant. The fetcher takes no signal argument: Fn() -> Fut instead of Fn(QuerySignal) -> Fut. This means your fetcher cannot observe cancellation. Use this if you have existing fetch closures that do not need signal awareness. For new code, prefer use_query (the signal-accepting version). use gpui_query::{use_query_unsignalled, CachePolicy, RequestPolicy}; let (entity, sub) = use_query_unsignalled( "settings", CachePolicy::Ttl { ttl_ms: 60_000 }, RequestPolicy::LatestWins, || async { let resp = reqwest::get("/api/settings").await?; Ok::<_, MyError>(resp.json().await?) }, cx, ); use_query_manual fn use_query_manual( key: QueryKey, cache_policy: CachePolicy, request_policy: RequestPolicy, cx: &mut Context, ) -> (Entity>, Subscription) Sets up the entity and observation without starting a fetch. Use this when you need full control over when and how fetching happens. The returned entity starts in Idle status with no data. You would call fetch_query or fetch_query_with_signal later to trigger a fetch on your own schedule. use gpui_query::{use_query_manual, fetch_query_with_signal, CachePolicy, RequestPolicy}; struct Dashboard { stats: Entity>, _sub: Subscription, } impl Dashboard { fn new(cx: &mut Context) -> Self { let (stats, _sub) = use_query_manual( "dashboard-stats", CachePolicy::Ttl { ttl_ms: 30_000 }, RequestPolicy::LatestWins, cx, ); Self { stats, _sub } } fn refresh(&mut self, cx: &mut Context) { fetch_query_with_signal(&self.stats, |signal| async move { let resp = reqwest::get("/api/stats").await?; Ok::<_, MyError>(resp.json().await?) }, cx); } } use_query_with_policy fn use_query_with_policy( options: impl Into, fetcher: F, cx: &mut Context, ) -> (Entity>, Subscription) where F: Fn(QuerySignal) -> Fut + Send + 'static, Fut: Future, E>> + Send + 'static, Like use_query, but the fetcher returns Result, E> instead of Result. Everything else matches use_query. Use this when a response carries its own caching directive that should override the caller's per-query CachePolicy ("server wins"). On success, wrap your data in Fetched: - Fetched::new(data): keep the resource's existing cache policy. - Fetched::with_policy(data, policy): replace the resource's policy with the server's, effective immediately after the fetch resolves. Subsequent freshness and SWR checks use the server's TTL. use gpui_query::{use_query_with_policy, QueryOptions, Fetched}; let (users, _sub) = use_query_with_policy( QueryOptions::new("users"), |signal| async move { let resp = reqwest::get("/api/users").await?; // Parse Cache-Control BEFORE consuming `resp` via `.json()` (which takes `self`). let policy = gpui_query_http::cache_policy_from_headers(resp.headers())?; let users: Vec = resp.json().await?; Ok::<_, MyError>(Fetched::with_policy(users, policy)) }, cx, ); The plain Result use_query is unchanged. Reach for use_query_with_policy only when the server can return a CachePolicy (typically HTTP APIs with Cache-Control / Expires headers). See the HTTP caching guide (/docs/guides/http-caching) for cache_policy_from_headers, and Caching: server wins with Fetched (/docs/guides/caching#server-wins-with-fetchedt) for the full server-wins contract. fetch_query fn fetch_query( entity: &Entity>, fetcher: F, cx: &mut Context, ) where F: Fn() -> Fut + Send + 'static, Fut: Future> + Send + 'static, Imperative refetch on an existing entity. Call this on button click, timer, or any event that should trigger a refresh. The fetcher takes no signal. On failure, retries according to the entity's retry policy. fn handle_refresh(&mut self, cx: &mut Context) { fetch_query(&self.users, || async { let resp = reqwest::get("/api/users").await?; Ok::<_, MyError>(resp.json().await?) }, cx); } fetch_query_with_signal fn fetch_query_with_signal( entity: &Entity>, fetcher: F, cx: &mut Context, ) where F: FnOnce(QuerySignal) -> Fut + Send + 'static, Fut: Future> + Send + 'static, Like fetch_query, but the fetcher receives a QuerySignal. The fetcher signature is FnOnce(QuerySignal) -> Fut. Because the closure is FnOnce, it is consumed on the first call. Retries are not possible. If you need retry logic, use fetch_query or use_query with a retry policy instead. fn handle_refresh(&mut self, cx: &mut Context) { fetch_query_with_signal(&self.users, |signal| async move { let resp = reqwest::get("/api/users").await?; // signal.is_cancelled() can be checked for long operations Ok::<_, MyError>(resp.json().await?) }, cx); } fetch_query_with_policy fn fetch_query_with_policy( entity: &Entity>, fetcher: F, cx: &mut Context, ) where F: Fn() -> Fut + Send + 'static, Fut: Future, E>> + Send + 'static, Like fetch_query, but the fetcher returns Result, E> so a server-derived CachePolicy can override the resource's policy on success. The fetcher takes no signal, same as fetch_query. On failure, retries according to the entity's retry policy. Use this for imperative refetches whose response should update the cache policy, for example a manual refresh of an HTTP endpoint. fn handle_refresh(&mut self, cx: &mut Context) { fetch_query_with_policy(&self.users, || async { let resp = reqwest::get("/api/users").await?; let policy = gpui_query_http::cache_policy_from_headers(resp.headers())?; let users: Vec = resp.json().await?; Ok::<_, MyError>(Fetched::with_policy(users, policy)) }, cx); } See use_query_with_policy (#use_query_with_policy) for the server-wins contract, and the caching guide (/docs/guides/caching#server-wins-with-fetchedt) for details. QueryResource QueryResource is the state container for a single query. It is stored as a GPUI entity and read during render. The type parameters are your data type T and error type E (defaults to QueryError). Reading state Method Return type Description data() Option<&T> The fetched data, if available error() Option<&E> The last error, if any status() QueryStatus Current lifecycle status is_loading() bool true when status is LoadingEmpty or LoadingWithData has_data() bool true when data is Some display_data() Option<&T> Data if present, otherwise placeholder data key() &QueryKey The query's cache key cache_age_ms(now_ms) Option Milliseconds since last successful update retry_count() u32 How many retries have been attempted in this cycle display_data() is the recommended accessor for UI rendering. It returns actual data when available, falling back to placeholder data during loading transitions. fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let users = self.users.read(cx); match users.status() { QueryStatus::LoadingEmpty => div().child("Loading..."), QueryStatus::Failure => div().child(format!("Error: {:?}", users.error())), _ => { if let Some(data) = users.display_data() { div().children(data.iter().map(|u| /* render user */ )) } else { div().child("No data") } } } } Data lifecycle methods Method Description placeholder_data() Returns the placeholder data, if set previous_data() Returns the previous data before the last successful update initial_data() Returns the seeded initial data, if any rollback_to_previous() Restores previous data, transitions to Success. Returns false if no previous data exists set_data(data) Optimistic update: stores current data as previous, sets new data set_placeholder_data(data) Sets placeholder data shown by display_data() during loading set_initial_data(data, now_ms) Seeds data when status is Idle and data is None set_data() is the core mechanism for optimistic updates. It stores the current data in previous_data so you can call rollback_to_previous() if the mutation fails. // Optimistic update before a mutation entity.update(cx, |r, _| { r.set_data(optimistic_users.clone()); }); // If mutation fails, roll back entity.update(cx, |r, _| { r.rollback_to_previous(); }); Cache and request control Method Description invalidate() Clears last_updated_at, causing the next fetch to bypass cache reset() Resets everything to Idle: data, error, status, retry count, signal cancel(error) Cancels the active request, sets status to Cancelled, stores the error cache_policy() Returns the current CachePolicy request_policy() Returns the current RequestPolicy cache_hits() Number of times cache short-circuited a fetch cancelled_count() Number of cancelled requests active_request_id() The RequestId of the in-flight request, if any // Invalidate to force a refetch on next mount entity.update(cx, |r, _| { r.invalidate(); }); // Cancel an in-flight request entity.update(cx, |r, _| { r.cancel(MyError::Cancelled); }); QueryStatus pub enum QueryStatus { Idle, // No fetch has started LoadingEmpty, // Fetch in progress, no data yet LoadingWithData, // Fetch in progress, previous data still available Success, // Fetch completed successfully Failure, // Fetch failed Cancelled, // Fetch was cancelled } The is_loading() method returns true for both LoadingEmpty and LoadingWithData. The distinction matters for UI: LoadingWithData means you can still show stale data while the fresh data loads. QueryOptions let opts = QueryOptions::new("users") .cache_policy(CachePolicy::Ttl { ttl_ms: 300_000 }) .request_policy(RequestPolicy::LatestWins) .retry_policy(RetryPolicy::new(3).with_exponential_backoff()) .gc_time(600_000) .force() .keep_previous(); QueryOptions implements From<&str>, From, and From, so you can pass a plain string when you do not need any customization. Builder method Default Description cache_policy(p) Ttl { ttl_ms: 60_000 } How cached data is treated request_policy(p) LatestWins How concurrent requests are handled retry_policy(p) 3 retries, exponential backoff Automatic retry on failure gc_time(ms) 300_000 (5 min) How long idle resources stay in cache force() false Bypass cache on the next fetch keep_previous() false Use previous data as placeholder when key changes See Caching (/docs/guides/caching) for details on cache policies and GC behavior. See Retry (/docs/guides/retry) for retry configuration. QuerySignal pub struct QuerySignal { /* Arc */ } impl QuerySignal { pub fn cancel(&self); pub fn is_cancelled(&self) -> bool; } A cooperative cancellation token backed by Arc. Clones share the same underlying flag. When any clone calls cancel(), all clones observe is_cancelled() == true. The library creates a fresh signal each time a fetch begins. The fetcher should check is_cancelled() periodically and abort early when possible. use_query("large-dataset", |signal| async move { let mut results = vec![]; for page in 0..100 { if signal.is_cancelled() { return Err(MyError::Cancelled); } let chunk = fetch_page(page).await?; results.extend(chunk); } Ok(results) }, cx); Signals are cancelled automatically when a newer request replaces an older one (the LatestWins request policy), or when you call cancel() or reset() on the resource. CachePolicy pub enum CachePolicy { NoCache, // Never cache Ttl { ttl_ms: u64 }, // Cache for N milliseconds StaleWhileRevalidate { ttl_ms: u64, stale_ms: u64 }, // Serve stale, refetch in background } The default is Ttl { ttl_ms: 60_000 } (one minute). - NoCache: every mount triggers a fresh fetch. - Ttl: cached data is served if cache_age_ms is within ttl_ms. Once expired, a new fetch starts. - StaleWhileRevalidate: cached data is always served (even after TTL expires), and a background refetch is triggered. The UI never shows a loading state for cached data. See Caching (/docs/guides/caching) for the full picture. RequestPolicy pub enum RequestPolicy { LatestWins, // New requests cancel older in-flight ones IgnoreWhileLoading, // New requests are dropped while one is in-flight } The default is LatestWins. Use IgnoreWhileLoading when you want to avoid redundant network traffic and the first response is good enough. QueryFetchMode pub enum QueryFetchMode { Normal, // Respect cache policy Force, // Always fetch, ignore cache } Set through QueryOptions::force(). Normal is the default. Force bypasses cache freshness checks and always starts a new fetch. QueryKey let simple: QueryKey = QueryKey::from("users"); let compound: QueryKey = QueryKey::from(["users", "42", "posts"]); Keys are hierarchical, modeled after TanStack Query's array keys. Internally stored as Arc<[Arc]>, so cloning is a single atomic ref-count increment. Two operations use this structure: exact match for cache lookups, and prefix match via starts_with() for cache invalidation. A key like ["users", "42"] matches the prefix ["users"] but not ["users", "43"]. QueryKey implements From<&str>, From, From<[&str; N]>, From>, and From>.