valentyn-vb / valentyn-vb/FoodNote

Move dashboard data fetching to Server Components and Server Actions

Open
#79 4 comments 0 reactions 1 assignee View on GitHub

@riedel28 is already working on this.

Since Jul 28, 2026.

Dominant language
TypeScript
Stars
1
Forks
1
PR merge metrics
No merged PRs in 30d

Description

Goal/Context

The frontend fetches all app data client-side through hand-rolled providers. MealsProvider and WeightProvider each run a useEffect with a cancelled flag, keep their own status: 'loading' | 'error' | 'ready', expose a retry() driven by a reloadKey, and fan mutations out through callbacks (onWeightSaved, onWeightUpdated, onWeightRemoved, refetchDashboard). That callback surface is cache invalidation written by hand: every mutation has to know which other read models it invalidates.

It already produces bugs. In #75, onWeightUpdated patches the provider's 60-day array in place, so re-dating an entry outside that window leaves a phantom row in the drawer and the client-computed trend while the server-derived goal block disagrees — until F5. canDelete has the same root cause: it counts the fetched window rather than the journal.

Next's recommended shape removes the class rather than the instance — the server re-renders from source after every mutation, so there is no client cache to drift. This ticket does the dashboard only; profile and onboarding stay on the current pattern until this is proven.

Blocker, already cleared. No server-side fetch can currently authenticate — the session PR (#88, #92, #108, sequenced in #110) moves the tokens into two Next-owned httpOnly cookies and puts serverFetch in lib/server/, so by the time this ticket starts, a Server Component can authenticate. This ticket does not touch auth.

Scope

This is PR 5 of the eight in #110 — the pilot slice, and the one that deletes both providers. Because it deletes them, everything else that consumes them comes with it, whether or not it is "the dashboard".

  • Reads move to lib/server/, on top of the serverFetch that PR 1 already shipped — no data module of this ticket's own, and no cookie handling here. Responses keep going through the shared/ Zod schemas, and per #89 that parsing lives only in the server data layer.
  • dashboard/page.tsx becomes async — fetches dashboard, meals and weights with Promise.all, passes plain data as props into <MobileDashboard> / <DesktopDashboard>. Both layouts stay client components; only the data source changes. Per #89, server data crosses as props, never through context, and is read at page level rather than in the layout.
  • meals/page.tsx comes too. It renders useMeals().todayMeals and throws the instant MealsProvider is deleted, so it cannot be a later slice. It is small and read-only: one server read, its status === 'error' branch becomes error.tsx, its skeleton becomes loading.tsx.
  • (app)/layout.tsx loses both providers. It keeps one deliberate, written-down exception to #89's rule 4: it reads getCurrentUser(), because identity does not vary by route.
  • meal-log-drawer.tsx, weight-drawer.tsx, weight-history-row.tsx, goal-reached-overlay.tsx are all provider or api-client consumers mounted inside (app) — including from app-sidebar.tsx, which is why they cannot be deferred. GoalReachedOverlay needs no redesign (#89: it is already state-derived); it just receives props instead of calling useMeals().
  • Server Actions in dashboard/actions.ts for weight create/update/delete and meal create. Per #90: react-hook-form keeps the form, the action is the write transport, dispatched inside startTransition with isPending as the only pending state; expected failures are ActionResult<T> return values, never throws, applied into RHF through setError via applyActionError. Deletes onWeightSaved / onWeightUpdated / onWeightRemoved / refetchDashboard / retry / reloadKey.
  • Revalidation needs re-deciding against Next 16, not copied from this ticket's first draft: per #87, revalidateTag now requires a cacheLife profile and no longer re-renders on its own, and updateTag / refresh are new. cacheComponents is out of scope map-wide (#93), so the use cache family is unavailable. Pick from what the previous caching model actually offers and say why in the PR.
  • loading.tsx + per-section <Suspense> replace states.tsx; error.tsx replaces DashboardError / InlineError. Per #92 the skeleton has the shape of its own route, and the sidebar paints instantly because the layout sits outside the boundary. use-dashboard-gate.ts goes away — its "no goal block means not renderable" branch becomes requireOnboarded(), which PR 1 already shipped.
  • Both providers are deleted outright (#89) — they hold server data and no UI state, so there is no reduced form to keep. MealsProvider may need to survive for the sidebar's "Log a meal" trigger: the trigger keeps its own open/closed state and submits through an action.
  • The weight-history drawer's on-open read passes the journal down from the page (#89) — the list is small. A Route Handler is the fallback if it ever isn't.

Out of scope

Auth of any kind — cookies, proxy.ts, gates and redirects all landed in PR 4 (#110). Profile and onboarding routes, meal edit/delete (#36's remaining half), and any backend change. Deleting api-client.ts and the transitional bridge is PR 8, not this one. npm test stays untouched; the e2e suite is blocking by now (#91) and this PR must keep it green — that is the entire reason the net went in first.

Done when

In the browser: a dashboard reload renders data with no loader flash and no client-side fetch in the network tab before hydration; editing a weight moves the trend, the change stat and the goal tile together with no invalidation wiring in the client; deleting is gone after F5; re-dating an entry outside the 6-week window removes it from the drawer immediately, and the phantom row from #75 cannot be reproduced. No useEffect fetch remains in the dashboard path, and /meals still renders after both providers are gone.

An expired session lands on /login via a server redirect, not a client bounce — true, but PR 4 proves it, not this one.

Dependencies

Everything in #110's PRs 1 through 4 — the lib/server/ and lib/actions/ infrastructure, the e2e net, the freeze, and the session move. #75 is merged; #68 (PR #76) and #106 both land before the freeze, and all three touch desktop-dashboard.tsx, mobile-dashboard.tsx and dashboard-charts.tsx, which this rewrites. The dashboard is frozen from the freeze PR until this one merges — see #110 for the exact paths.

Supersedes the "adopt TanStack Query" idea: Server Actions plus server re-render cover the same ground, so don't do both.

Guidance

No new dependencies — this is framework surface, not a library. Every dashboard read is per-user and cookie-dependent, so it is dynamic and uncached; the win is streaming and removed round-trips, not cache hits, and the use cache family is unavailable to us anyway (#93).

One tension to settle: AGENTS.md's Forms section vs action={serverAction} + useActionState — settled by #90, and the answer is not the Next-idiomatic one. react-hook-form keeps the form; the Server Action is only the write transport. No progressive enhancement, no action={…} wiring, no useActionState, and formState.isSubmitting is never read again. Write against that, not against the framework docs' default form.

frontend/AGENTS.md is binding: read the relevant guide in node_modules/next/dist/docs/ before writing code or asserting an API — this Next is not the one in your training data. The facts this migration bets on are written up in docs/research/next-16-facts.md (#87).

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.