Skip to main 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 actually breaks.

Summary of what changed

Areav1v2
Primary call stylePositional args (key, cache, request, fetcher)Options-first (QueryOptions::new(key), fetcher)
Hook return typeEntity only(Entity, Subscription) tuple
Fetcher signalOptionalAlways receives a QuerySignal
Error interopQueryError without Error implQueryError: Display + Error (? and anyhow)
Mutation retriesCould retry by defaultDefault to no retries
Infinite max_pagesUnboundedDefaults to Some(50)
Mutation GCEffectively a no-opReal GC, respects gc_time_ms
Status dedupRe-rendered on every mutationQueryObserver 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

  • 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).

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