demo(ai-studio): swap the live queries for ReactFire - #800
Open
tyler-reitz wants to merge 4 commits into
Open
Conversation
…itch The export hard-codes the Google-internal project makersuite-showcase with a named Firestore database, so it cannot run anywhere else. Repoints the config and adds VITE_USE_EMULATORS so the same build serves local verification and a real project later. The named database id is kept: the emulator serves named databases (verified 2026-08-20 by write and isolation, since a read probe returns 200 from any database name). tsconfig gains a types array because the export never referenced vite/client, so import.meta.env did not typecheck; the other three entries keep the ambient types the no-types default was already loading. Verified against the emulators: console sign-in with an unsigned Google credential lands on onboarding, and the negative control (emulator down) surfaces testConnection's offline error in the console, which stops on restart. The UI is not the signal for that control; the sign-in screen renders normally either way. Separate from the ReactFire swap so that diff shows only the swap.
Providers first with nothing consuming them, so any breakage here is attributable before a conversion is layered on top. The dependency is the same pinned build recipe-demo uses (main at ac3ccf9), extracted from git with matching hashes, and the gitignore negation is proven both ways: the demo tarball is visible to git while a root-level one stays ignored. Verified in the browser against the emulators: sign-in, household creation, onboarding exit and the seeded recipe grid all behave exactly as the unwrapped app, with no console errors. npm ls react shows one react@19.2.4 with every consumer deduped to it.
The hook cannot be called conditionally and the query needs a signed-in user, so the subscription lives in HouseholdsFeed, mounted behind a user guard. One mount point is not enough: App renders four mutually exclusive screens, and the feed must be mounted in every one a signed-in user can reach (spinner, onboarding, main), or the app deadlocks on onboarding with the subscription unmounted and households stuck empty. The selection logic moves into a useCallback handler unchanged. The !user bail-out and the loading-true-on-user-change behavior are reproduced exactly, including leaving a stale household list on sign-out as the original did. The onSnapshot error callback is gone: useObservable re-throws rather than surfacing an error status, so Firestore errors reach the app's ErrorBoundary (main screen) or nothing (spinner and onboarding screens) instead of handleFirestoreError. Verified against the emulators in the browser: existing user renders, fresh user exits onboarding by creating a household through the UI (the deadlock case), an external rename of the selected household streams into the header with no reload, and the idField mutation control blanks the page with a where() error, proving the feed is what drives the list.
Both subscriptions now come from ReactFire. Neither hook can be called conditionally, so each lives in a child component mounted only when its precondition holds, in every return branch a signed-in user can reach: the surrounding selection logic and sort are unchanged and merely moved out of the snapshot callbacks. The sort operates on a copy, since sorting ReactFire's data in place would mutate its cached value. The now-unused onSnapshot import is dropped. The onSnapshot error callbacks are gone. useObservable re-throws rather than surfacing an error status, so Firestore errors now reach the app's ErrorBoundary instead of handleFirestoreError. Verified against the emulators in the browser: switching households switches recipe lists, an external write appears sorted first with no reload, createdAt-descending order holds across probe and seeded recipes, no-household-selected (onboarding) stays distinguishable from household-with-no-recipes (empty grid), and the mutation control (dropping the handler's data) empties the grid, restored and re-verified after.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Swaps the AI Studio export's two live Firestore subscriptions for ReactFire hooks. The diff against
ai-studio-demois the deliverable: same app, same behavior, nothing added, the 1426-lineApp.tsxkept as one file per the requirements doc.Not a merge candidate. Comparison artifact, like its base.
What converted and what did not
onSnapshothouseholdsHouseholdsFeedonSnapshotrecipesRecipesFeedonAuthStateChangeduseUseranduseSigninChecktherefore never appear in this demo; project 1 covers bothgetDocs×4toggleLikein project 1. Zero diff is the honest outputThe headline: hooks cannot be conditional, and one mount point is not enough
Both original effects bail out early (
if (!user) return,if (!user || !selectedHousehold) return). A hook cannot opt out of running, so each subscription became a child component mounted behind its guard, pointing at #346 (disabling queries, 8 reactions) and #463 (nullable refs, 18 reactions).It is worse than one extraction per subscription:
Apprenders four mutually exclusive screens, and the feeds must be mounted in every branch a signed-in user can reach, three of the four. Mounted only in the main return, the app deadlocks on onboarding:householdsis empty, so the onboarding screen renders, the feed never mounts, and nothing can ever populatehouseholds; verified by the onboarding-exit check, which hangs in that configuration and passes in this one. The vanilla effect ran regardless of which screen rendered. Rebuilding that property by hand is part of the swap's cost.The measurement
+106 / -42 across 3 files (
App.tsx+96/-41,main.tsx+9/-1,package.json+1), measured from the repoint commit so the config changes are not counted against the swap. The net +64 lines was the predicted outcome, stated in the spec up front: for an app shaped like this one, ReactFire pushes generated code toward more structure, not less. On the token question the doc actually asks: extrapolating from this diff, a generator writing this app with ReactFire available would have spent more tokens on the data layer, not fewer. That is an estimate derived from the line delta, not a generation measurement.Other findings
useObservablere-throws unconditionally, so bothhandleFirestoreErrorcallbacks are gone: Firestore errors now reach the app'sErrorBoundaryfrom the main screen, and nothing at all from the spinner and onboarding screens. Same finding as project 1; fix: surface observable errors via status instead of re-throwing #735 (v5-only) fixes exactly this.instructions) blanks the whole app. Reproduced accidentally during verification with a malformed probe document; both halves crash identically.datain place would mutate its cached value.Verification
Against the Firebase emulators, in the browser, each check paired with a control:
createdAtdescending across probe and seeded recipesidFieldbroken → blank page with awhere()error; recipes handler neutered → empty grid. Both restored and re-verifiedtscandvite buildUnverified:
signInWithPopup(needs a focused window and a human; sessions were established viasignInWithCredentialagainst the Auth emulator), Gemini generation (needs the AI Studio key), and the export's ownfirestore.rules: every check ran under the repo's open emulator rules, so the app's real security model is unexercised on both halves equally.