Skip to content

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.

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)

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]));

Two methods make hierarchical keys useful:

  • starts_with(prefix): true when this key begins with prefix. This is the primitive the bulk operations use to invalidate a whole subtree.
  • to_path(): a users::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");

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!()
}
}

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);
# }
  • Exact when you know the full key (for example, after a mutation that updates a single record). invalidate_queries(Exact(&["users", "42"])) refetches only that user.
  • Prefix when 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.
  • All for global resets, most commonly on logout: remove_queries(All) clears the cache, and reset_queries(All) clears data and cancels in-flight requests.

Keep the list and a detail view under a shared prefix so one invalidation refreshes both:

let list = QueryKey::from(["users"]); // the list
let detail= QueryKey::from(["users", id.as_str()]); // one user

After an update mutation, invalidate_queries(Prefix(&["users"])) refreshes the list and any cached detail pages in one call.

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"]);

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”.

  • QueryClient: the bulk operations that consume a QueryKeyFilter.
  • Caching: how CachePolicy interacts with keys.
  • Error handling: surfacing failures keyed by resource.