Skip to content

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

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 }));
}
}

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.

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:

todo-list-cache.ts
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;
}
}

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:

  1. Prefer the ambient context. Call helpers in field initializers and let the component own them. Cleanup is then automatic and correct.
  2. When you must use injectRef, pass an injector whose lifetime is the lifetime you actually want. A component-scoped EnvironmentInjector unsubscribes with the component; the root injector does not.
  3. 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.
  4. If you need a genuinely disposable scope, create one with createEnvironmentInjector() and destroy it yourself.
  • Reactivity — the effects and DestroyRef cleanup that injectRef retargets.
  • Providers — the injector that holds the CONVEX token in the first place.