Quick Start
This page walks through a minimal end-to-end query: define a fetcher, subscribe to it with use_query, read the status and data, and render. Then a tiny use_mutation example so you can see the full loop.
It assumes you have already installed gpui-query and set up a QueryClient global.
The mental model
Section titled “The mental model”gpui-query is built around three pieces:
- A fetcher: an async function that knows how to load some data. gpui-query owns everything else: caching, deduplication, retries.
use_query: a hook you call from a view’s constructor (not inrender). It fetches the first time, subscribes the view to the result, and returns anEntityyou read during render.- The
QueryClient: a global registry that shares results across views so the same key is only ever fetched once.
Data is cached under a QueryKey. Two views asking for the same key read from the same cached entity.
Your first query
Section titled “Your first query”1. Model your data and error
Section titled “1. Model your data and error”Both the data type T and the error type E must be Clone + Send + Sync + 'static, and E must be Debug. A cheap stand-in for the error is the crate’s QueryError, but you can use your own.
#[derive(Clone, Debug)]pub struct Todo { pub id: u32, pub title: String, pub done: bool,}2. Define a fetcher
Section titled “2. Define a fetcher”A fetcher is a Fn() -> Future<Output = Result<T, E>>. It takes no arguments and returns the data or an error. The real networking is up to you (reqwest, an LSP transport, a local file). Here we return a fixed list so the example compiles without a server.
use gpui_query::QueryError;
async fn fetch_todos() -> Result<Vec<Todo>, QueryError> { // Replace this with a real request. Ok(vec![ Todo { id: 1, title: "Write the docs".into(), done: false }, Todo { id: 2, title: "Ship it".into(), done: true }, ])}3. Subscribe with use_query
Section titled “3. Subscribe with use_query”Call use_query in your view’s new. It returns a tuple: the resource entity (store it on the view to read during render) and a Subscription (store it too, because dropping it stops the view from re-rendering on changes).
use gpui_query::hook::use_query;use gpui_query::{CachePolicy, QueryError, QueryResource, core::RequestPolicy};
pub struct TodosView { todos: gpui::Entity<QueryResource<Vec<Todo>, QueryError>>, _sub: gpui::Subscription,}
impl TodosView { pub fn new(cx: &mut gpui::Context<Self>) -> Self { let (todos, sub) = use_query( "todos", // cache key CachePolicy::Ttl { ttl_ms: 60_000 }, // fresh for 1 minute RequestPolicy::LatestWins, // supersede in-flight fetches || async { fetch_todos().await }, // fetcher: Fn() -> Fut cx, ); Self { todos, _sub: sub } }}use_query starts a fetch the first time the resource is seen, and reuses the cached entity on every subsequent access for the same key. Pass a fresh QueryKey (QueryKey::from(["todos"]) for a multi-segment key) when you want hierarchical keys for invalidation.
4. Read status and data, then render
Section titled “4. Read status and data, then render”A QueryResource<T, E> is a normal GPUI entity. Read it with entity.read(cx) and branch on status():
impl gpui::Render for TodosView { fn render(&mut self, _window: &mut gpui::Window, cx: &mut gpui::Context<Self>) -> impl gpui::IntoElement { let r = self.todos.read(cx);
match r.status() { gpui_query::QueryStatus::Idle | gpui_query::QueryStatus::LoadingEmpty => { // First load, no data yet. gpui::div().child("Loading…") } gpui_query::QueryStatus::Success => { let list = r.data().cloned().unwrap_or_default(); gpui::div().children( list.into_iter().map(|t| gpui::div().child(t.title)), ) } gpui_query::QueryStatus::Failure => { let msg = r.error().map(|e| format!("{e:?}")).unwrap_or_default(); gpui::div().child(format!("Failed: {msg}")) } _ => gpui::div(), } }}Useful predicates on the resource:
r.is_loading(): true during the first fetch or any refetch.r.is_pending(): true when there is no data yet and a fetch is in flight.r.data()/r.error():Option<&T>/Option<&E>, borrowed for the duration of the read.
That is the whole query loop: define a fetcher, subscribe with use_query, branch on status, render. gpui-query handles caching, deduplication, and re-rendering on every status transition.
A tiny mutation
Section titled “A tiny mutation”Mutations are imperative: you trigger them on demand (a button, a form submit). use_mutation creates a MutationResource<V, T, E> entity: V is the variables you pass in, T the success result, E the error. Trigger it with the mutate helper.
use gpui_query::hook::{use_mutation, mutate};use gpui_query::{MutationResource, MutationStatus, QueryError};
#[derive(Clone)]pub struct ToggleTodo { pub id: u32 }
pub struct TodoActions { toggle: gpui::Entity<MutationResource<ToggleTodo, Todo, QueryError>>,}
impl TodoActions { pub fn new(cx: &mut gpui::Context<Self>) -> Self { // use_mutation returns the entity; turbofish fixes the (V, T, E, C) quadruple. let toggle = use_mutation::<ToggleTodo, Todo, QueryError, Self>(cx); Self { toggle } }
pub fn on_toggle(&mut self, id: u32, cx: &mut gpui::Context<Self>) { mutate( &self.toggle, ToggleTodo { id }, // variables |vars| async move { // mutator: Fn(V) -> Fut // Replace with a real PATCH/POST. Ok(Todo { id: vars.id, title: "Toggled".into(), done: true }) }, cx, ); }}Read the mutation’s status the same way as a query:
# use gpui_query::{MutationResource, MutationStatus, QueryError};# #[derive(Clone)] struct ToggleTodo { id: u32 }# #[derive(Clone)] struct Todo { id: u32, title: String, done: bool }# fn doc(m: &gpui::Entity<MutationResource<ToggleTodo, Todo, QueryError>>, cx: &gpui::App) {match m.read(cx).status() { MutationStatus::Idle => { /* render a button */ } MutationStatus::Loading => { /* disable the button, show a spinner */ } MutationStatus::Success => { /* confirm */ } MutationStatus::Failure => { /* render a retry button */ }}# }Mutations default to no retries. After a mutation that changes server state, invalidate the affected queries so they refetch:
# use gpui_query::client::QueryClient;# use gpui_query::core::{QueryKey, QueryKeyFilter};# fn doc(cx: &mut gpui::App) {# let mut client = cx.global::<QueryClient>().clone();let todos = QueryKey::from(["todos"]);client.invalidate_queries(&QueryKeyFilter::Prefix(&todos), cx);# }Where to go next
Section titled “Where to go next”You now have a query that caches and a mutation that triggers. From here:
- Queries: the full
use_querysurface, the fetcher signal, and refetching. - Mutations: lifecycle callbacks and the
mutatevariants. - Caching:
CachePolicy, request deduplication, and GC. - Query keys: hierarchical keys and invalidation scope.