Skip to content

Prewarming queries

injectPrewarmQuery() opens a short-lived background subscription for a query and its arguments. Nothing is rendered from it — the point is that the result lands in the Convex client’s local cache, so an injectQuery() for the same query and args a moment later reads a warm value instead of waiting for a round trip.

The natural trigger is intent: a hover, a focus, a pointerdown on a link.

function injectPrewarmQuery<Query extends PrewarmQueryReference>(
query: Query,
options?: PrewarmQueryOptions<Query>,
): PrewarmQueryResult<Query>;
interface PrewarmQueryResult<Query extends PrewarmQueryReference> {
prewarm: (args: Query['_args']) => Promise<boolean>;
}

Note the shape: the query is bound once, and prewarm() is called with the arguments. One helper covers every row in a list.

user-list.component.ts
import { Component, inject } from '@angular/core';
import { Router, RouterLink } from '@angular/router';
import { injectPrewarmQuery } from 'convex-angular';
import { api } from './convex/api';
@Component({
selector: 'app-user-list',
imports: [RouterLink],
template: `
@for (id of userIds; track id) {
<a [routerLink]="['/users', id]" (mouseenter)="onIntent(id)" (focus)="onIntent(id)" (click)="open(id, $event)">
{{ id }}
</a>
}
`,
})
export class UserListComponent {
private readonly router = inject(Router);
readonly userIds = ['user-1', 'user-2'];
readonly prewarmProfile = injectPrewarmQuery(api.users.getProfile, {
// Hold the warm subscription long enough to cover an unhurried click.
extendSubscriptionFor: 10_000,
onError: (error, args) => console.warn('prewarm failed for', args.userId, error),
});
// Fire-and-forget on intent: the promise is not interesting here.
onIntent(userId: string): void {
void this.prewarmProfile.prewarm({ userId });
}
// Or await it to hold navigation until the cache is warm. `prewarm()`
// resolves false on failure, expiry, or scope destruction — navigate
// either way and let the routed component's own query handle it.
async open(userId: string, event: Event): Promise<void> {
event.preventDefault();
await this.prewarmProfile.prewarm({ userId });
await this.router.navigate(['/users', userId]);
}
}
Name Type Default Description
injectRef EnvironmentInjector ambient injector Injector used to create the helper outside the current injection context. Its destruction disposes every active prewarm.
extendSubscriptionFor number 5000 Milliseconds to keep the background subscription alive after prewarm() is called.
onError (err: Error, args: Query['_args']) => void Invoked when a background subscription fails. Receives the args that were prewarmed.

The window is a trade-off. Too short and the cache is already cold by the time the user clicks; too long and you hold subscriptions for rows the user never visits. The 5 second default covers a normal hover-then-click. For a list where hovering is incidental, shorten it; for a deliberate “open in a moment” flow, lengthen it.

onError receiving the args matters for a shared helper: without them you cannot tell which row’s prewarm failed.

prewarm(args: Query['_args']): Promise<boolean>;
Resolves When
true The background subscription received its first result. A subsequent injectQuery() for the same query and args will read the warm cache.
false The subscription failed.
false The extendSubscriptionFor window expired before any result arrived.
false The owning scope (component or injectRef injector) was destroyed before a result arrived.
false Server-side rendering — the client is disabled and prewarming is a no-op.

The promise settles once, at whichever of those happens first. A subscription that expires after a result already arrived leaves the promise resolved true.

The promise never rejects, so void prewarm(...) is a safe fire-and-forget call. On the error path the subscription is disposed immediately rather than waiting out the timer.

readonly prewarmProfile = injectPrewarmQuery(api.users.getProfile);
onIntent(userId: string): void {
void this.prewarmProfile.prewarm({ userId });
}
async open(userId: string): Promise<void> {
await this.prewarmProfile.prewarm({ userId });
await this.router.navigate(['/users', userId]);
}

Wire onIntent to mouseenter and focus so keyboard users get the same benefit, and let the click handler either navigate immediately (the cache is probably warm already) or await the prewarm for a guaranteed-instant render. Because prewarm() resolves false rather than rejecting, the await form degrades to a normal loading state instead of blocking navigation.

Server-side rendering needs no guard: prewarm() short-circuits on a disabled client and resolves false immediately, so it never registers a timer that would delay SSR stability.

Both warm the cache before a component renders; they differ in who waits.

injectPrewarmQuery convexQueryResolver
Trigger user intent, anywhere route activation
Blocks navigation no (unless you await it yourself) yes, until the first result
Bounded yes, extendSubscriptionFor no timeout on resolution
Best for hover preloading, speculative warming routes that must never render empty