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.
Signature
Section titled “Signature”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.
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]); }}PrewarmQueryOptions
Section titled “PrewarmQueryOptions”| 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.
The prewarm() contract
Section titled “The prewarm() contract”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.
The hover-or-intent pattern
Section titled “The hover-or-intent pattern”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.
Prewarming vs. the route resolver
Section titled “Prewarming vs. the route resolver”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 |