Query Keys
Every cached resource in gpui-query is identified by a QueryKey, a structured, hierarchical key inspired by TanStack Query’s array keys (["users", "42", "posts"]). Keys drive caching, deduplication, and the bulk operations (invalidate_queries, cancel_queries, reset_queries, remove_queries) that the QueryClient exposes.
Creating a key
Section titled “Creating a key”A QueryKey is an ordered sequence of string segments. The simplest case is a single string:
use gpui_query::QueryKey;
let key = QueryKey::from("users");For resources scoped by an identifier, build a multi-segment key. The key records the order of the segments, so ["users", "42"] and ["42", "users"] are different keys.
let user_key = QueryKey::from(["users", "42"]);let user_posts = QueryKey::from(["users", "42", "posts"]);let post_detail = QueryKey::from(["posts", "7"]);You can construct a key from any of:
&str/String(single segment)[&str; N],Vec<&str>,Vec<String>(multiple segments)QueryKey::new(iterator)(panics on an empty iterator: a key must have at least one segment)
Cheap cloning
Section titled “Cheap cloning”Internally a QueryKey stores its segments in an Arc<[Arc<str>]>, so cloning a key is a single atomic ref-count increment regardless of how many segments it holds. The hook layer and the QueryClient both clone keys freely (into buckets, into fetch tasks, into diagnostics).
let key = QueryKey::from(["users", "42", "posts", "comments"]);let cloned = key.clone();assert!(std::sync::Arc::ptr_eq(&key.parts()[0], &cloned.parts()[0]));Prefix matching
Section titled “Prefix matching”Two methods make hierarchical keys useful:
starts_with(prefix):truewhen this key begins withprefix. This is the primitive the bulk operations use to invalidate a whole subtree.to_path(): ausers::42::posts-style display string for DevTools and logs. The::separator avoids ambiguity when a segment itself contains a forward slash.
use gpui_query::QueryKey;
let posts = QueryKey::from(["users", "42", "posts"]);
assert!(posts.starts_with(&QueryKey::from(["users"])));assert!(posts.starts_with(&QueryKey::from(["users", "42"])));assert!(!posts.starts_with(&QueryKey::from(["posts"])));
assert_eq!(posts.to_path(), "users::42::posts");Using keys with hooks
Section titled “Using keys with hooks”The options-first hooks accept anything that converts Into<QueryKey>, so you can pass a bare string for the simple case or a structured key for a scoped resource:
use gpui_query::hook::use_query;use gpui_query::{QueryOptions, QueryKey};# #[derive(Clone)]# struct User;# #[derive(Clone, Debug)]# struct MyError;
impl View { fn new(cx: &mut gpui::Context<Self>) -> Self { // Single string — the common case. let (users, _) = use_query("users", |_signal| async move { Ok::<_, MyError>(vec![]) }, cx);
// Structured key for a scoped resource. let (user, _) = use_query( QueryKey::from(["users", "42"]), |_signal| async move { Ok::<_, MyError>(User) }, cx, );
// Equivalent, via QueryOptions: let _ = use_query( QueryOptions::new(["users", "42", "posts"]), |_signal| async move { Ok::<_, MyError>(vec![]) }, cx, ); // ...# unimplemented!() }}QueryKeyFilter
Section titled “QueryKeyFilter”The bulk operations on QueryClient do not take a raw key; they take a QueryKeyFilter, which is how you say which keys to act on. There are three variants:
| Filter | Matches |
|---|---|
Exact(k) |
Only the key that is exactly equal to k. |
Prefix(k) |
Every key that starts_with(k): the whole subtree. |
All |
Every key in the cache. |
QueryKeyFilter is Copy, so you can build it inline at the call site:
use gpui_query::client::QueryClient;use gpui_query::core::{QueryKey, QueryKeyFilter};
# fn _doc(client: &mut QueryClient, cx: &mut gpui::App) {// Invalidate everything under the "users" subtree — every user,// every user's posts, etc.let users = QueryKey::from(["users"]);client.invalidate_queries(&QueryKeyFilter::Prefix(&users), cx);
// Invalidate just one specific resource.let one = QueryKey::from(["users", "42"]);client.invalidate_queries(&QueryKeyFilter::Exact(&one), cx);
// Throw away all cached query state (e.g. on logout).client.remove_queries(&QueryKeyFilter::All);# }Choosing a filter
Section titled “Choosing a filter”Exactwhen you know the full key (for example, after a mutation that updates a single record).invalidate_queries(Exact(&["users", "42"]))refetches only that user.Prefixwhen a mutation affects a family of resources. For example, after creating a user,invalidate_queries(Prefix(&["users"]))refetches the users list and every cached user.Allfor global resets, most commonly on logout:remove_queries(All)clears the cache, andreset_queries(All)clears data and cancels in-flight requests.
Patterns
Section titled “Patterns”Scoped keys for list + detail
Section titled “Scoped keys for list + detail”Keep the list and a detail view under a shared prefix so one invalidation refreshes both:
let list = QueryKey::from(["users"]); // the listlet detail= QueryKey::from(["users", id.as_str()]); // one userAfter an update mutation, invalidate_queries(Prefix(&["users"])) refreshes the list and any cached detail pages in one call.
Dynamic segments
Section titled “Dynamic segments”QueryKey only stores strings. To scope by a non-string id, format it into the key:
let key = QueryKey::from(["projects", &project_id.to_string(), "members"]);Avoid empty keys
Section titled “Avoid empty keys”QueryKey::new with a zero-length iterator panics. Every key must have at least one segment; there is no “root” key. Use QueryKeyFilter::All when you mean “everything”.
Next steps
Section titled “Next steps”QueryClient: the bulk operations that consume aQueryKeyFilter.- Caching: how
CachePolicyinteracts with keys. - Error handling: surfacing failures keyed by resource.