Quick Start
This walks through installing gpui-query and making your first query. You need a GPUI project already set up. If you don't have one, follow the installation page first.
Add the dependency
Put this in your Cargo.toml:
[dependencies]
gpui-query = { version = "0.1.0", features = ["hook"] }
The hook feature pulls in use_query, use_mutation, and the rest of the declarative API. Without it you get the raw client layer, which is useful but more manual.
Set up the query client
QueryClient manages all cached state. It needs to live as a GPUI global so any view can access it. Create it once during app startup:
use gpui::App;
use gpui_query::QueryClient;
App::new().run(|cx| {
cx.set_global(QueryClient::new());
// ... your views
});
That is the entire setup. One line. The client handles garbage collection, cache eviction, and request deduplication internally.
Make your first query
Call use_query from inside a view. It takes a key, a fetcher closure, and the view context. It returns an Entity<QueryResource<T, E>> and a Subscription.
use gpui_query::use_query;
use gpui::Entity;
struct MyView {
query_entity: Entity<QueryResource<Vec<User>, MyError>>,
_subscription: Subscription,
}
impl MyView {
fn new(cx: &mut ViewContext<Self>) -> Self {
let (entity, sub) = use_query(
"users",
|signal| async move {
let users = fetch_users().await?;
Ok::<Vec<User>, MyError>(users)
},
cx,
);
Self {
query_entity: entity,
_subscription: sub,
}
}
}
The string "users" is the cache key. Two views calling use_query with the same key share the same cached data. The fetcher only runs once, not twice.
The signal argument inside the closure is a QuerySignal. Call signal.is_cancelled() to check if the request was invalidated and you should bail early. For simple cases you can ignore it.
Read the state in render
QueryResource<T, E> tracks the full lifecycle of a request: idle, loading, success, failure. Read it from render with read_with:
use gpui_query::QueryStatus;
impl Render for MyView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let entity = self.query_entity.clone();
entity.read_with(cx, |resource| {
match resource.status() {
QueryStatus::LoadingEmpty => "Loading...",
QueryStatus::Success => "Got data",
QueryStatus::Failure => "Error",
_ => "Idle",
}
})
}
}
The status() method returns one of Idle, LoadingEmpty, LoadingHasData, Success, Failure, or Cancelled. When data arrives, resource.data() returns Some(&T). On failure, resource.error() returns Some(&E).
Mutations
Queries fetch data. Mutations change it. use_mutation gives you an entity that starts idle and only runs when you explicitly trigger it:
use gpui_query::{use_mutation, mutate};
let (entity, sub) = use_mutation((), cx);
// Trigger it later, on a button click or form submit
mutate(&entity, NewUser { name: "Alice" }, |vars| async move {
Ok::<User, MyError>(create_user(vars).await)
}, cx);
Mutations do not touch the query cache on their own. To refresh the user list after creating a user, call invalidate_queries in a success callback. See Mutations for the full API.