Skip to content

Latest commit

 

History

History
83 lines (51 loc) · 6.16 KB

File metadata and controls

83 lines (51 loc) · 6.16 KB
AngularFireDeveloper Guide ❱ Zone Wrappers

Zone Wrappers

AngularFire wraps the framework agnostic Firebase JS SDK and RxFire to ensure proper functionality in Zone and Zoneless applications alike.

These wrappers ensure Firebase APIs are called outside of the Angular zone. This isolates side-effects such as timers so that they do not destabilize your application.

Observables, Promise-based APIs, and those with callbacks will purposely destabilize your application until a initial value is returned, this ensures that server-side rendering (SSR) and static site generation (SSG/pre-rendering) wait for data before rendering the page's HTML.

Consequences of not Zone wrapping

When using a Firebase or RxFire API without importing from AngularFire or if AngularFire APIs are used outside of an injection context you may experience instability.

When an application is unstable change-detection, two-way binding, and rehydration may not work as expected—leading to both subtle and non-subtle bugs in your application. Further, server-side rendering (SSR) and static site generation (SSG/pre-rendering) may timeout or render a blank page.

There are a number of situations where AngularFire's Zone wrapping is inconsequential such adding/deleting/updating a document in response to user-input, signing a user in, calling a Cloud Function, etc. So long as no long-lived side-effects are kicked off, your application should be ok. Most Promise based APIs are fairly safe without zone wrapping.

Keeping calls inside an injection context

A common way to trip the warning is calling an AngularFire API from inside an asynchronous callback, for example building a Firestore query inside a switchMap on the signed-in user. The surrounding service is created inside an injection context, but the callback runs later, after that context is gone, so AngularFire can no longer wrap the call.

Capture an EnvironmentInjector while the context is still active (any field initializer or constructor), then re-establish it inside the callback with runInInjectionContext:

private readonly injector = inject(EnvironmentInjector);

readonly todos$ = this.user$.pipe(
  switchMap((user) =>
    runInInjectionContext(this.injector, () =>
      collectionData(collection(this.firestore, `users/${user.uid}/todos`)),
    ),
  ),
);

Inside runInInjectionContext, AngularFire can wrap the call again, so the SSR/change-detection guarantees are restored and the warning goes away.

If a call doesn't depend on the async value, prefer hoisting it out of the callback entirely and running it once where the injection context is still active:

// Built once where the context is live, then reused, so no wrapping is needed:
private readonly items$ = collectionData(collection(this.firestore, "items"));

readonly refreshed$ = this.refresh$.pipe(switchMap(() => this.items$));

Reach for runInInjectionContext only when the call genuinely must run inside the callback, as the per-user query above does (it needs the signed-in user's uid).

Why the injection context is required

AngularFire cannot create an injection context, only borrow the one you are already in.

When you call a wrapped API, the first thing AngularFire does is ask Angular for three things with inject(): its own scheduler service, Angular's PendingTasks register, and an EnvironmentInjector. inject() only works inside an injection context, so outside of one the first of them throws and the rest are never reached. AngularFire catches that, warns while in dev-mode as described under Logging below, and calls the Firebase API directly with nothing added.

That division is worth knowing, because it explains what you are and are not responsible for:

  • You supply the context at the call site, from a field initializer, a constructor, or an explicit runInInjectionContext.
  • AngularFire re-enters the environment injector inside a callback you hand to the call itself, which is why you never wrap an onSnapshot handler yourself. That reaches anything provided at the application level, but not providers declared on a component, and it does not extend to subscribers of a returned Observable, which is what the switchMap and runInInjectionContext example above is for.

What you lose depends on the call. A call that does asynchronous work, such as onSnapshot, getDoc or collectionData, is normally added to Angular's PendingTasks register, which is what server-side rendering waits on before it serializes the page. Called outside a context, it never is. A call that returns immediately, such as getFirestore, was never registered anyway, so it loses only the zone handling.

Logging

You may see a log warning, Calling Firebase APIs outside of an Injection context may destabilize your application leading to subtle change-detection and hydration bugs. Find more at https://github.com/angular/angularfire/blob/main/docs/zones.md when developing your application.

Instability can be difficult to track down. To help with debugging, AngularFire emits warnings when it is unable to Zone wrap an API while in dev-mode. Often these messages can be safely ignored but we'd rather be verbose.

There are three logging levels in AngularFire:

  • Silent: when the logging level is set to silent only the above banner is displayed when AngularFire APIs are called outside of an injection context, this is the default when using Zoneless change-detection.
  • Warn: when the logging level is set to warn, only blocking reads, long-lived tasks, and APIs with high risk of destabilizing your application are logged, this is the default when using ZoneJS.
  • Verbose: when the logging level is set to verbose, all AngularFire APIs called outside of an injection context are logged—helping you track down APIs that may be destabilizing your application

You can change the log-level like so:

import { setLogLevel, LogLevel } from "@angular/fire";

setLogLevel(LogLevel.VERBOSE);