Skip to content

refactor: recipe demo app rebuilt on AngularFire- #3753 - #5

Open
armando-navarro wants to merge 8 commits into
recipe-demofrom
recipe-demo-framework
Open

refactor: recipe demo app rebuilt on AngularFire- #3753#5
armando-navarro wants to merge 8 commits into
recipe-demofrom
recipe-demo-framework

Conversation

@armando-navarro

Copy link
Copy Markdown
Owner

This is the same application as the recipe-demo branch, with its Firebase layer rebuilt on AngularFire.

It is 8 commits, 13 files, and 249 lines added against 262 removed. Each commit converts one area, so the diff can be read a commit at a time.

What AngularFire replaces

In plain terms

  • The app no longer wires up Firebase services by hand. The base branch builds four wiring objects and threads them through three config files. This branch declares each Firebase product it uses in a single line.
  • Watching who is signed in used to be a subscription the app opened, held onto and closed. It is now one line, and Angular closes it.
  • Reading the live recipe list used to be a listener the app managed itself, with its own success path, error path and cleanup. It is now one declaration.
  • Disposing of the per-request Firebase instance on the server used to be an explicit delete call. It is now a property on the call that creates the instance.
  • One workaround the base branch needs for local development goes away, because the library already does it.

In detail

  • A firebase-tokens.ts holding four InjectionTokens, wired through five hand-written provider objects across three config files, becomes six provide* calls. The file is deleted.
  • An onAuthStateChanged subscription, the signal it pushed into, the teardown that unsubscribed it and a whenAuthResolved() wrapper become one toSignal(authState(auth), { initialValue: null }).
  • A resource whose loader opened onSnapshot by hand, pushed snapshots and errors into a signal and registered the unsubscribe function as an abort listener on the loader's abortSignal, becomes an rxResource over collectionData.
  • An explicit deleteApp in a DestroyRef.onDestroy block becomes a releaseOnDeref property, following the pattern in docs/auth.md.
  • The App Check debug-token flag the base branch sets by hand on localhost goes away, because provideAppCheck sets it already.

Across the four files that hold the Firebase layer, the base branch is 397 lines and this branch is 364.

What it does not change

These are invisible in a diff, and each is a place a reader could otherwise draw a stronger conclusion than the change supports.

  • The hand-rolled TransferState handoff carrying the recipe list from server to browser. Angular's automatic state transfer covers HttpClient, which Firestore never touches, and AngularFire adds nothing here, so the same hand-written code is needed on both branches.
  • All of server.ts, including the __session cookie plumbing behind the signed-in server render.
  • beforeAuthStateChanged, which has no observable form in the library and stays imported from firebase/auth on both branches.
  • The hasAuthIdToken type predicate that narrows REQUEST_CONTEXT.
  • The signed-in server render itself, which the base branch already had. What the diff shows is that feature being converted, not gained.

What it gains

Less code is the visible part. These are the things the app stops being responsible for.

  • Teardown becomes the framework's job. Two subscriptions the base branch has to store and unsubscribe itself are now torn down by Angular. There is no unsubscribe function to keep hold of, so there is none to forget.

  • Initialization order comes free. provideFirestore lists Auth and App Check among its own dependencies, with the library's comment reading "Firestore+Auth work better if Auth is loaded first", so Firestore resolves after Auth without the app arranging it. The base branch owns that ordering by hand.

  • The error path arrives with the read. The base branch passes an explicit error callback to onSnapshot and pushes the error into a signal itself. rxResource surfaces it without that.

  • The App Check debug token is handled more broadly than the hand-written version. The base branch sets the flag for an exact localhost match. provideAppCheck sets it whenever the app is in development mode or the hostname is localhost, 0.0.0.0 or 127.0.0.1. Worth an honest caveat: this app's angular.json allows only localhost, so the wider coverage cannot actually be observed here.

  • The server's wait for auth stops being explicit. The base branch waits with an app initializer calling authStateReady(). Here authState is zone-wrapped, and that wrapper registers the pending task that holds the server render open, so the initializer is deleted. One precision worth stating, because it is easy to over-credit: the recipe list's own server wait comes from Angular's rxResource, not from AngularFire. Auth is the place where AngularFire demonstrably carries the wait in this app.

What it costs

  • The recipe read becomes unvalidated. collectionData(query, { idField: 'id' }) returns untyped documents, so the store crosses them to the app's Recipe type with two casts and nothing checks the shape at runtime. The base branch used a FirestoreDataConverter.
  • Every AngularFire-wrapped call needs an injection context, so the two stores gain an injected EnvironmentInjector and wrap their calls in runInInjectionContext. Nine call sites, six in the recipe store and three in the auth store. Template event handlers are not injection contexts, which is why the write paths need this and the base branch does not.

Running it

Deployed instance: https://angularfire--recipe-demo-97859.us-central1.hosted.app

recipe-demo/README.md on this branch covers the project setup, the run commands, the design decisions that are hard to deduce from the code, and the gaps a production app would close that this one deliberately leaves open.

What happens to this pull request

This should never be merged. It exists as a reference demo.

Commit b83343d is the latest canary as of this writing.
…ders

The four hand-made injection tokens and their platform guards are gone,
along with the file that declared them. AngularFire's providers take
over: provideFirebaseApp once per platform, plus provideFirestore,
provideAuth, provideAppCheck and provideAI.
The browser's cookie sync moves from a raw onIdTokenChanged subscription
with a manual teardown to AngularFire's idToken observable with
takeUntilDestroyed. Only the beforeAuthStateChanged half still needs an
unsubscribe of its own.

The server factory no longer builds a Firebase server app for anonymous
requests. A server app carries two things into a render, a user identity
and a pre-minted App Check token, and an anonymous request has neither
here, so a plain app does the same job with nothing to release. App Check
is enforced only on AI Logic, which this app calls from the browser alone.
The signed-in path hands the request context to releaseOnDeref, so the SDK
drops the app once that object is collected, and the explicit deleteApp
teardown goes with it.
AuthStore drops its hand-rolled onAuthStateChanged subscription, its
signal and its teardown for one toSignal over authState. The guard
stops going through AuthStore and reads the same observable directly,
because a signal cannot distinguish "not resolved yet" from "resolved,
signed out". Both read null, and a guard that confuses them sends a
signed-in visitor to the sign-in page.

The server config loses its provideAppInitializer that awaited
authStateReady. AngularFire holds a pending task open from the moment
authState is subscribed until its first emission, and AuthStore
subscribes as soon as a component injects it, so the render already
waits without the initializer.

The two are not the same guarantee. The initializer blocked bootstrap,
so nothing ran until auth resolved. A pending task only defers the
point at which the render counts as stable, so work can still run
before auth resolves. Data reads are unaffected, because Firestore
waits for its own credentials.
The browser reads recipes through a live listener via collectionData.
The server performs a single read via getDocs. rxResource is used to
more easily work with AngularFire's RxJs observables.

AngularFire-wrapped functions that need an injection context are
wrapped in runInInjectionContext.

The document-reading converter goes. Dropping it is what makes the read
return untyped documents, and the two casts that follow are the price.
They are deliberate and they are the measured cost of the current API,
which is why they are written rather than worked around.

TransferState stays hand-wired: the state key, the set on the server,
the signal seeded from it, the computed that prefers the transferred
list, and the clearing once the listener answers. AngularFire supplies
none of those.
The likes read becomes an rxResource over collectionData, keeping an
explicit readiness flag. The flag resets in finalize alone, which
works because rxResource tears the old stream down before subscribing
the new one.

The writes now import from AngularFire, which has a cost. An
AngularFire-wrapped function needs an injection context.
Anything the two branches share is inherited from branch 1 unchanged. A
README difference that AngularFire did not cause is noise in the diff
that is the whole deliverable.

The production gaps are deliberate and each says what it costs. The
unvalidated read is the current state of the library rather than a
workaround, and the two casts it needs are named.
The inherited config named raw-sdk, which is the other branch's live
site. firebase init apphosting made that worse rather than better,
turning the apphosting field into an array holding both backends, so a
deploy could have reached the wrong one.

allowedHosts carried the same four raw-sdk hostnames. Without replacing
them the deployed site answers HTTP 400 to every request.

apphosting.yaml is deliberately not in this commit. The init rewrote it
with a newer template, twenty lines of added comments and no change to
any setting, and reverting it keeps the branch-to-branch diff free of
noise the CLI introduced rather than AngularFire.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant