Injection context
Every inject* helper in this library resolves dependencies (the ConvexClient, DestroyRef,
PLATFORM_ID) and creates effects. Both require Angular’s injection context, exactly like the
built-in inject().
The rule
Section titled “The rule”Call the helpers where Angular is in an injection context:
- a component, directive, or service field initializer
- a constructor
- inside
runInInjectionContext(...) - inside a factory provider
@Component({ /* … */})export class TodoListComponent { // Field initializer: an injection context. Correct. readonly todos = injectQuery(api.todos.list, () => ({ count: 20 }));
loadMore() { // A method body is NOT an injection context. This throws. const more = injectQuery(api.todos.list, () => ({ count: 100 })); }}What it throws
Section titled “What it throws”When a helper is called outside an injection context and no explicit injector was supplied, it throws an error naming the offending helper:
<helperName>() must be called from an injection context (for example, a component or service field initializer or constructor), or be given an explicit injector via the `injectRef` option to create it later from plain code.<helperName> is the real function name, so calling injectQuery from a method body produces
injectQuery() must be called from an injection context …. This replaces Angular’s generic
NG0203 message specifically to point at the injectRef option.
The injectRef escape hatch
Section titled “The injectRef escape hatch”Every helper accepts an optional injectRef: EnvironmentInjector in its options object:
| Helper | Options type |
|---|---|
injectConvex() |
InjectConvexOptions |
injectQuery() |
QueryOptions |
injectQueries() |
QueriesOptions |
injectPaginatedQuery() |
PaginatedQueryOptions |
injectPrewarmQuery() |
PrewarmQueryOptions |
injectMutation() |
MutationOptions |
injectAction() |
ActionOptions |
injectConvexConnectionState() |
InjectConvexConnectionStateOptions |
injectAuth() |
InjectAuthOptions |
injectBetterAuth() from convex-angular/better-auth accepts an injectRef too, but it only uses
it to resolve BetterAuthService from that injector — it creates no effects of its own.
When injectRef is present, the helper skips the injection-context assertion entirely and runs its
whole setup inside runInInjectionContext(injectRef, …). When it is absent, the helper asserts the
ambient context and runs there, so ownership stays with the caller’s scope. An explicit injectRef
always wins over an ambient context, even when one exists.
Capture the injector where you are in an injection context, then use it later from plain code:
import { EnvironmentInjector, Injectable, inject } from '@angular/core';import { QueryResult, injectQuery } from 'convex-angular';
import { api } from './convex/api';
type TodoListQuery = QueryResult<typeof api.todos.list>;
@Injectable({ providedIn: 'root' })export class TodoListCache { // Captured while we ARE in an injection context (a field initializer). private readonly injector = inject(EnvironmentInjector);
private readonly byCount = new Map<number, TodoListQuery>();
// Called later from plain code — there is no ambient injection context here, // so `injectQuery` would throw without `injectRef`. forCount(count: number): TodoListQuery { const cached = this.byCount.get(count); if (cached) { return cached; }
// Ownership follows `injectRef`, not the caller: this subscription lives // until the root injector is destroyed, and is cached so repeated calls do // not open a new subscription every time. const query = injectQuery(api.todos.list, () => ({ count }), { injectRef: this.injector }); this.byCount.set(count, query); return query; }}injectRef overrides ownership
Section titled “injectRef overrides ownership”This is a real leak. The pattern below looks harmless and is not:
@Injectable({ providedIn: 'root' })export class TodoService { private readonly injector = inject(EnvironmentInjector); // the ROOT injector}
@Component({ /* … */})export class TodoListComponent { private readonly service = inject(TodoService);
// Called on every click. Each call opens a subscription owned by the root // injector — destroying this component unsubscribes none of them. refresh() { injectQuery(api.todos.list, () => ({ count: 20 }), { injectRef: this.service.injector }); }}Guidance:
- Prefer the ambient context. Call helpers in field initializers and let the component own them. Cleanup is then automatic and correct.
- When you must use
injectRef, pass an injector whose lifetime is the lifetime you actually want. A component-scopedEnvironmentInjectorunsubscribes with the component; the root injector does not. - If the call site can run more than once, cache the result (as the example above does) so repeated calls reuse one subscription instead of stacking new ones.
- If you need a genuinely disposable scope, create one with
createEnvironmentInjector()and destroy it yourself.
Related
Section titled “Related”- Reactivity — the effects and
DestroyRefcleanup thatinjectRefretargets. - Providers — the injector that holds the
CONVEXtoken in the first place.