Skip to main content

The Select Pattern

TanStack Query's select option transforms cached data into a derived shape, re-running only when the underlying data changes. gpui-query provides the same pattern through SelectTransform and the use_query_select hook. One cached query can feed several derived views without duplicating the cache entry.

Why a separate hook?

In Rust the transform output type U has to be known at compile time. The QueryOptions struct is not generic over a transform output, so instead of making the whole options type generic, select lives on a dedicated hook: use_query_select. It accepts the transform as a separate parameter and returns a MappedQueryResource<T, U, E> entity.

The trade-off is ergonomic: select is a distinct hook rather than a field on options, but the type system stays simple.

SelectTransform

A SelectTransform<T, U> wraps an Fn(&T) -> U closure. Build one with SelectTransform::new:

use gpui_query::core::SelectTransform;

// Count the users in a cached Vec<User>.
let count = SelectTransform::new(|users: &Vec<String>| users.len());

// Project a single field.
let names = SelectTransform::new(|users: &Vec<String>| {
users.iter().cloned().collect::<Vec<_>>()
});

assert_eq!(count.apply(&vec!["a".to_string(), "b".to_string()]), 2);

SelectTransform is Clone + Send + Sync (the closure is stored in an Arc), so you can share one transform across several mapped resources.

use_query_select

The hook wires a SelectTransform into the use_query lifecycle. It returns a triple: the mapped view entity, the underlying query entity, and the pair of subscriptions that keep both observations alive. Store both subscriptions — dropping either breaks the observation.

use gpui_query::hook::{use_query_select, QueryOptions};
use gpui_query::core::SelectTransform;
# #[derive(Clone, PartialEq)]
# struct User { name: String }
# #[derive(Clone, Debug)]
# struct MyError;

struct UserCountView {
/// Derived view: just the count.
count: gpui::Entity<gpui_query::core::MappedQueryResource<Vec<User>, usize, MyError>>,
_subs: (gpui::Subscription, gpui::Subscription),
}

impl UserCountView {
fn new(cx: &mut gpui::Context<Self>) -> Self {
let transform = SelectTransform::new(|users: &Vec<User>| users.len());

let (count, _query, subs) = use_query_select(
QueryOptions::new("users"),
transform,
|_signal| async move { Ok::<_, MyError>(vec![]) },
cx,
);

Self { count, _subs: subs }
}
}

Reading the derived value applies the transform lazily:

# // (inside a render / event handler that has cx)
# let count: &gpui::Entity<gpui_query::core::MappedQueryResource<Vec<String>, usize, ()>> = unimplemented!();
# let cx: &gpui::App = unimplemented!();
let n: Option<usize> = count.read(cx).data();

How it stays in sync

use_query_select does three things:

  1. Calls use_query to create and subscribe to the underlying QueryResource<T, E>.
  2. Seeds a MappedQueryResource<T, U, E> with the query's current data (one clone into an Arc<T>).
  3. Observes the source entity. Every time it changes, the mapped resource compares the new data against the cached source by reference and only re-stores + notifies when the content actually changed.

The upshot: unchanged notifications (the common case — a retry tick, a status flip with the same data) pay a cheap Arc::clone instead of a full T clone. The transform itself is applied lazily, on every data() call.

Many views from one cache

Because the source data is shared, you can attach several mapped resources to the same underlying query, each with its own transform:

use gpui_query::core::SelectTransform;

let total = SelectTransform::new(|users: &Vec<String>| users.len());
let names = SelectTransform::new(|users: &Vec<String>| users.clone());
let first = SelectTransform::new(|users: &Vec<String>| users.first().cloned());

All three read from the same cached Vec<User>. There is no duplication — each MappedQueryResource stores only an Arc<T> to the shared source plus its own transform closure.

Cost notes

  • The transform closure runs every time MappedQueryResource::data() is called — there is no output cache. For lightweight transforms (a field access, a .len()) the cost is negligible.

  • For expensive transforms called more than once in a render pass, cache the result in a local:

    let derived = mapped.read(cx).data(); // transform runs once
    // reuse `derived` below instead of calling .data() again
  • The mapped entity notifies observers only when the source content changes, so derived views re-render at the same cadence as the underlying query — no extra churn from retry counts or status flips that leave data untouched.

Next steps

  • Query keys — keys the underlying query is cached under.
  • Caching — how the source data's CachePolicy controls refetch.
  • Observers — the status-deduplication primitive the mapped resource relies on.