Migrating from v1 to v2
gpui-query v2 (the 0.2.x line) is an options-first redesign. The hooks keep the same names, but the call signatures, return types, and a number of internal behaviors changed. This page is a checklist for porting a v1 codebase to v2, grouped by what actually breaks.
Summary of what changed
| Area | v1 | v2 |
|---|---|---|
| Primary call style | Positional args (key, cache, request, fetcher) | Options-first (QueryOptions::new(key), fetcher) |
| Hook return type | Entity only | (Entity, Subscription) tuple |
| Fetcher signal | Optional | Always receives a QuerySignal |
| Error interop | QueryError without Error impl | QueryError: Display + Error (? and anyhow) |
| Mutation retries | Could retry by default | Default to no retries |
Infinite max_pages | Unbounded | Defaults to Some(50) |
| Mutation GC | Effectively a no-op | Real GC, respects gc_time_ms |
| Status dedup | Re-rendered on every mutation | QueryObserver deduplicates by status |
1. Switch to options-first
The single biggest mechanical change. v1 took the key, cache policy, and request policy as separate positional arguments. v2 takes a single QueryOptions (or just a key string, which converts via Into).
// v1
// let entity = use_query("users", cache_policy, request_policy, fetcher, cx);
// v2
use gpui_query::hook::use_query;
use gpui_query::QueryOptions;
let (entity, _sub) = use_query(
QueryOptions::new("users")
.cache_policy(cache_policy)
.request_policy(request_policy),
|signal| async move { Ok::<_, gpui_query::QueryError>(vec![]) },
cx,
);
For the simplest case — just a key with default policies — pass the string directly:
let (entity, _sub) = use_query("users", |signal| async move { Ok(vec![]) }, cx);
2. Handle the tuple return
v2 hooks return (Entity<…>, Subscription). You must store the Subscription or the observation is dropped immediately and your view will not re-render on data changes.
struct MyView {
users: gpui::Entity<gpui_query::QueryResource<Vec<User>, MyError>>,
_subscription: gpui::Subscription, // keep this
}
This applies to use_query, use_infinite_query, and use_mutation. use_query_select returns a triple including a second subscription for the mapped observer — store both.
3. Adopt the signal-always fetcher
v2 fetchers always receive a QuerySignal. If you have long-running fetches, check signal.is_cancelled() periodically to bail out early when a newer request supersedes yours. For short fetches you can ignore it — the two-phase accept_current_request guard discards stale results automatically.
If you cannot change the fetcher signature, use_query_unsignalled keeps the Fn() -> Fut (no-signal) shape for backward compatibility. It is not the recommended default.
4. Review mutation retry defaults
v1 mutations could retry by default. v2 mutations default to RetryPolicy::no_retries() because silently retrying a destructive action is dangerous. If your mutation is idempotent and you relied on retries, opt back in explicitly:
use gpui_query::{MutationOptions, RetryPolicy};
use gpui_query::hook::use_mutation;
let (entity, _sub) = use_mutation(
MutationOptions::default().retry_policy(RetryPolicy::new(2)),
cx,
);
5. use_mutation_with_options is deprecated
use_mutation_with_options(options, cx) is deprecated since 0.2.0. It now delegates to use_mutation, which accepts MutationOptions via Into. Replace it:
// v1
// let (entity, sub) = use_mutation_with_options(&opts, cx);
// v2
let (entity, sub) = use_mutation(opts, cx);
The deprecated function is retained (and still works) so existing callers compile, but it is no longer re-exported from the crate root. Move off it to clear the deprecation warning.
6. Bound infinite queries with max_pages
v1 infinite queries accumulated pages without limit. v2 defaults to max_pages: Some(50) to prevent unbounded memory growth from deep scrolling. If you genuinely need unbounded pages, opt back in:
use gpui_query::hook::InfiniteQueryOptions;
let opts = InfiniteQueryOptions::new("feed").unbounded_pages();
Otherwise, decide on a cap that fits your UI (max_pages(n)).
7. Take advantage of QueryError: Display + Error
v2 gives QueryError full Display + Error impls, so it composes with ? and anyhow. You can now return it from fallible helpers directly:
use gpui_query::QueryError;
fn lookup() -> Result<Vec<User>, QueryError> {
// ...
}
And use QueryError::sanitized() at the boundary where you convert server responses, so tokens and connection strings do not leak into logs or DevTools.
8. Trust status deduplication
v1 re-rendered views on every internal mutation (including retry ticks). v2's QueryObserver / MutationObserver deduplicates by status, so a retrying query no longer re-renders your view on every attempt — only on terminal status changes. You can remove manual "did the status actually change?" guards from your render logic.
What did not change
QueryKeyand the hierarchical key model are unchanged.QueryKeyFilter(Exact/Prefix/All) and the bulk operations onQueryClientare unchanged.CachePolicy,RequestPolicy, and their semantics are unchanged.- The
QueryClientas a GPUIGlobal, set viacx.set_global(QueryClient::new()), is unchanged. - Cancellation is still cooperative via
QuerySignal; only the staleness guard is stricter (no more TOCTOU window).
Need help?
If a v1 pattern is not covered here, the source of truth is the crate's own doc comments — every module lists its v2 changes at the top. Run cargo doc -p gpui-query --open for the item-level reference.
Next steps
- API reference — the flat index of v2 types and functions.
- Comparison: vs. raw async — what v2 removes versus hand-rolled
cx.spawn. - Quick start — the canonical v2 entry point.