Load the web console's server state through TanStack Query - #23
Merged
Merged
Conversation
Pull in @tanstack/react-query 5.102 and mount a shared client in main.tsx. The client's defaults live in src/lib/query-client.ts with their reasons: no retries, since nearly every failure here is a 4xx a retry cannot fix, and no refetch on window focus, since admin pages mount several queries at once. api() now announces successful writes (onSuccessfulWrite), and main.tsx has the cache drop the queries no screen is using. One write can change what several cached screens hold; this way none of them reopens onto data from before it, without each call site knowing what it touched. @tanstack/eslint-plugin-query joins the lint config: its exhaustive-deps keeps every value a queryFn reads in its key. Tests get renderWithQueryClient, which renders inside a fresh cache. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every load that carried a react-hooks/set-state-in-effect suppression goes through useQuery now, and writes invalidate what their screen shows instead of calling a loader again. The loading flags, the abort controllers, the fetch-token race guard and the 13 loader useCallback wrappers they needed are gone; query keys follow the endpoint. Behaviour that changes on purpose: - Logout, and the cross-tab logout broadcast, clear the cache along with the user. The signed-in user is one shared query, so signing out from Profile now signs out the whole UI. - A reload after a write refreshes in place instead of flashing the skeleton, and a load error clears on the next successful load. - RouteEditorDialog maps remote-models entries to ids: the endpoint returns objects, and the dialog handed them to React as children. - OutboxBacklogDialog drops onChanged: invalidating the webhook-outbox prefix also refreshes the parent's backlog counts. - Polling uses refetchInterval, which pauses while the tab is hidden. Where cached data can already be there on the first render, local state follows it through trackers that start empty: BatchImportDialog can mount open, the Models page's ?import= deeplink opens it during render, and the pricing form seeds once, from a fetch made after mount. BatchImportDialog also offers nothing until both lists are fresh, so a cached copy cannot offer models imported since. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
adminErrors was state kept in step with the form by an effect that ran validateAdmin after every change. It is a pure function of the form values, so it is a useMemo now: an error clears on the keystroke that fixes it, and the last set-state-in-effect suppression goes with the effect. It was labelled a hand-rolled load; it never fetched anything. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The "Data fetching" section described hand-rolled loads and the two reasons a suppression of set-state-in-effect could stand. Both are gone, so the section now describes the query layer — keys, writes, signals, deriving local state — and keeps the rule against new suppressions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Thanks for the PR. One thing before review: this targets
No need to close and reopen: click Edit next to the PR title and change the base branch to See CONTRIBUTING.md and docs/operations/release.md for the branch contract. |
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.
What
The web console's data loading moves from hand-rolled loaders onto TanStack Query v5. Every
react-hooks/set-state-in-effectsuppression is gone — 29 of them — and so are the 13 loaderuseCallbackwrappers #22 added so those effects could list honest dependencies.27 of the suppressions sat on loads. The other two had the same shape without a fetch — a setup-form validation effect and the Models page's
?import=deeplink — and are fixed the same way: derived during render instead of synced in an effect.Verified with the CI steps —
pnpm install --frozen-lockfile,check:i18n(1386 keys),lint(0), 103 tests (96 + 7 new),pnpm build— andmake precommit. The Playwright E2E suite needs a running backend and is not part of CI; it was not run for this change.The layer
src/lib/query-client.tsbuilds the client. Two defaults differ from the library's and say why: no retries (nearly every failure here is a 4xx a retry cannot fix, and three silent attempts hold a spinner up for seconds), and no refetch on window focus (admin pages mount several queries, some of them multi-page catalog loops).GET /api/admin/teams/{id}/members→['admin', 'teams', id, 'members'], query-string parameters in a trailing object.api()announces successful non-GET requests (onSuccessfulWrite) andmain.tsxhas the cache drop its inactive queries. A call site rarely knows everything a write touched — adding a team member moves the team list's count, the user list's team column and the team page — so no call site has to.@tanstack/eslint-plugin-queryjoins the lint config. Itsexhaustive-deps— a key must name everything itsqueryFnreads — is the query-layer counterpart of the honest effect dependencies Clear 127 lint findings and gate lint in CI #22 went for.renderWithQueryClient, which gives each call an empty cache.Behaviour worth a look in review
Screens load what they loaded before. These differences are deliberate, or fall out of keying responses:
useAuthstate used to be per component, so "revoke sessions" and "delete account" cleared only the profile page's copy of the user; the shell kept rendering until some later request 401'd. The signed-in user is one shared query now.RouteEditorDialog's upstream-model picker rendered the wrong shape.GET /api/admin/providers/{id}/remote-modelsreturns{ id, available?, reason? }objects since model verdicts landed; the dialog still treated them as strings and handed objects to React as children. It maps them to ids now.OutboxBacklogDialogloses itsonChangedprop. A retry or delete invalidates['admin', 'webhook-outbox'], which also covers the Log Forwarders page's backlog counts.refetchInterval, which pauses while the tab is hidden and resumes on the next interval once it is visible again.Where the cache changed the logic, not just the plumbing
BatchImportDialogopens on its first render. Its reset treats that as an open transition, whichuseResetOnChange(initialised to the first value) would not. Covered by a test.isFetchedAfterMount), never from a cached copy that could predate another admin's save, and a refetch never overwrites what is being typed.admin_sharedin a resumed MCP wizard discards the credential probe, including one still in flight, so its late answer cannot jump the wizard back to Step 3; switching back probes again.Counts
#22's description counts 18 + 9 suppressions and 17 loader wrappers. The tree had 18 + 11 suppressions, and 13 of #22's 17 new
useCallbacks wrap loaders — the other four (mergeOrderin stat-cards;goNext,goPrevandhandleSubmitin RoleWizard) are not loads and stay.Not in this PR
Loads that never tripped the rule —
useTeams, the dashboard's live panels, the command palette, a few dialogs — still fetch in effects. They can move over one at a time; the README describes the pattern.🤖 Generated with Claude Code