Skip to content

demo(recipe): swap the hand-rolled hooks for ReactFire - #798

Open
tyler-reitz wants to merge 3 commits into
recipe-demofrom
recipe-demo-framework
Open

demo(recipe): swap the hand-rolled hooks for ReactFire#798
tyler-reitz wants to merge 3 commits into
recipe-demofrom
recipe-demo-framework

Conversation

@tyler-reitz

@tyler-reitz tyler-reitz commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Swaps the vanilla app's hand-rolled Firestore and auth plumbing for ReactFire. The diff against recipe-demo is the deliverable: same app, same behavior, nothing added.

Not a merge candidate. This branch and #797 exist to be compared and read, not landed.

What ReactFire replaces

Deleted Replaced by
src/lib/use-recipes.ts, 44 lines of hydrate-then-subscribe useFirestoreCollectionData(query, { idField, initialData })
src/lib/session-context.tsx, 18 lines of context over onAuthStateChanged useUser() and useSigninCheck(), called directly

Nine files under src/ plus package.json, +42 / -94. The base branch's commit message predicts five files; that assumed keeping both modules as thin wrappers over ReactFire, which was rejected because it preserves the abstraction ReactFire is meant to obviate and hides useSigninCheck entirely. src/lib/providers.tsx (16 lines) is new and wires the three providers to the app's existing module-level instances. recipes.ts, types.ts, ai.ts, firebase.ts, RecipeList.tsx and all three pages are untouched.

The homepage still fetches on the server with the Firebase JS SDK, not the Admin SDK, and hands the result to a client component as initialData. The recipe list is public, so no identity is needed and the security rules stay in play.

Findings

This app does not build against published reactfire. SessionNav calls useUser from the root layout, so every prerendered route hits a reactfire hook:

Error occurred prerendering page "/_not-found"
Error: Missing getServerSnapshot, which is required for server-rendered content.
    at i (.next/server/app/signin/page.js:1:8950)

Measured both ways: patched build → 8 <article> in the server HTML; published 4.2.6 → build fails; patched build restored → 8 again. #779 is therefore a hard prerequisite for this demo, not an improvement to it.

Error handling is where reactfire is worse. RecipeBrowser had an error branch rendering the failure inline. useObservable on main re-throws unconditionally, suspense: false or not, so that UI cannot work and the branch is deleted rather than left dead. The vanilla hook kept the page up and showed the error; this version unmounts to the nearest boundary. #735 fixes exactly this and is v5-only, so it is the obvious candidate for a round two.

A cast is unavoidable. useFirestoreCollectionData<Recipe> requires a Query<Recipe>, but recipeQuery returns Query<DocumentData>, so the generic moves the error rather than solving it. Typing it properly means a withConverter in recipes.ts, which would put Firestore types back into the file the swap is meant to leave alone. data as Recipe[] at the call site is the smaller cost.

Hydrated versus live is off the status enum. The distinction the Firebase SSR blog post's hand-rolled useSerializedQuery tracks is not something reactfire's status can carry. Measured against the emulator with initialData passed: while the server data is still on screen, status is already success and hasEmitted is already true, and only firstValuePromise resolving marks the first live snapshot. So the capability exists, but only by awaiting a promise and mirroring it into local state, which is the bookkeeping the swap is meant to remove. The vanilla branch previously declared four feed states while rendering two; both sides now carry the same three, and this is the reason why.

Two pages are byte-identical to vanilla, which is also a result: signin/page.tsx, since there is no sign-in wrapper, and create-recipe/page.tsx, since there is no AI Logic binding and the page calls the SDK directly. RecipeCard changes only its user lookup; the like itself is plain updateDoc either way. RequireAuth barely shrinks, because useSigninCheck still returns a status and the loading-then-redirect dance survives.

The dependency

reactfire is a packed build of main at ac3ccf9, committed under recipe-demo/ with a .gitignore exception. Regenerate with npx tsc && npx vite build && npm pack at that commit. A file:.. link was rejected because npm symlinks the repo root in, bringing a second React with it.

Verification

Against the emulator suite, controls run in both directions:

Check Result
Server render 8 <article> with <script> blocks stripped, negative control on an absent title
Cuisine filter Japanese returns exactly 2
Like Like (0)Liked (1), page sentinel survived so no reload
Live subscription An external write from a node script appeared as Like (1) with no interaction, and rendered as "Like" rather than "Liked" because a different uid made it
Route protection /create-recipe signed out redirects to /signin?next=%2Fcreate-recipe
Console No errors and no hydration warnings; the reader was confirmed able to report by planting one
tsc Clean, confirmed able to fail first

Not verified: /create-recipe generation, which needs a real project with AI Logic enabled. Same gap as the vanilla half.

AGENTS.md and CLAUDE.md in this folder are written by next dev when it detects an AI coding agent, never by next build, and agentRules: false turns the behaviour off. They are Next's own instructions to coding agents, not ours, and are committed so the tree stays clean.

Replaces the app's own Firestore and auth plumbing with ReactFire hooks,
so the diff against recipe-demo is the comparison itself.

- src/lib/use-recipes.ts (42 lines) becomes useFirestoreCollectionData
  with initialData, keeping the server-fetched list as the seed
- src/lib/session-context.tsx (17 lines) is deleted; useUser and
  useSigninCheck are called from the components that need them
- src/lib/providers.tsx wires FirebaseAppProvider, FirestoreProvider and
  AuthProvider to the existing module-level instances

The dependency is a packed build of main at ac3ccf9, committed under
recipe-demo/ with a gitignore exception. Published 4.2.6 predates #779,
and without it this app does not build: SessionNav calls useUser from the
root layout, so prerendering /_not-found and /signin fails with "Missing
getServerSnapshot". Measured both ways, with the patched build restored
afterwards.

RecipeBrowser loses its error branch because useObservable on main
re-throws rather than returning status 'error', and the recipe list needs
a cast because useFirestoreCollectionData's generic requires a typed
Query. Both are findings for the PR description rather than problems to
work around here.
# Conflicts:
#	recipe-demo/src/lib/use-recipes.ts
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