Skip to content

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

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

The biggest mechanical change is the call signature. v1 took the key, cache policy, and request policy as separate positional arguments. v2 takes a single QueryOptions (or a bare 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 (a key with default policies), pass the string directly:

let (entity, _sub) = use_query("users", |signal| async move { Ok(vec![]) }, cx);

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.

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.

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. Mutation options moved into use_mutation

Section titled “5. Mutation options moved into use_mutation”

The old options-taking mutation hook, deprecated in 0.2.0, has been removed. use_mutation accepts MutationOptions via Into, so one hook covers both shapes:

// with options
let (entity, sub) = use_mutation(opts, cx);
// with defaults (no retries, 5 minute GC)
let (entity, sub) = use_mutation((), cx);

If you were still on the deprecated hook, switch to the opts form above; the call reads the same and nothing else changes.

v1 infinite queries accumulated pages without limit. v2 defaults to max_pages: Some(50) to prevent unbounded memory growth from deep scrolling. If you 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)).

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.

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.

  • QueryKey and the hierarchical key model are unchanged.
  • QueryKeyFilter (Exact / Prefix / All) and the bulk operations on QueryClient are unchanged.
  • CachePolicy, RequestPolicy, and their semantics are unchanged.
  • The QueryClient as a GPUI Global, set via cx.set_global(QueryClient::new()), is unchanged.
  • Cancellation is still cooperative via QuerySignal; only the staleness guard is stricter (no more TOCTOU window).

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.