Background
useEntityMutation (entity-api.ts:464) auto-invalidates SWR cache entries after a successful mutation by matching keys whose value contains the entity's baseUrl:
mutate((key) => typeof key === "string" && key.includes(baseUrl));
The typeof key === "string" guard silently skips tuple-keyed caches. useEntityList (entity-api.ts:357) uses tuple keys ([url, params]) so SWR can reuse the same URL with different query params as distinct cache entries:
const key = actualParams ? [url, actualParams] : null;
Net effect: every list fetched via useEntityList is invisible to the auto-invalidate path. After any mutation (create / update / delete), the corresponding lists do not refresh until either a hard reload or a manual refresh() call.
This was surfaced in #386 (the dashboard CoachingSessionsCard delete flow) and worked around at the call site, but it affects every useEntityList consumer in the codebase.
Proposed fix
Update the predicate to handle both shapes:
mutate((key) => {
if (typeof key === "string") return key.includes(baseUrl);
if (Array.isArray(key) && typeof key[0] === "string") return key[0].includes(baseUrl);
return false;
});
Scope
Lists that would newly auto-invalidate (desired effect)
11 tuple-keyed useEntityList hooks across: agreements, coaching-relationships, coaching-sessions (×2 — relationship-scoped + user-scoped enriched), goal-progress, goals (×2), organizations, organizations/users, user-actions, users.
In every case the new behavior is what callers want — the manual refresh() calls scattered across the codebase exist precisely because this didn't work.
Manual refresh() cleanup (the real work of this issue)
After the predicate fix, ~15 manual refresh() call sites become redundant. Each needs verification that auto-invalidation alone is sufficient (same SWR key, no scope-specific edge cases) before removal:
| Location |
Currently fires |
src/components/ui/dashboard/coaching-session-form.tsx |
session list refresh after create/update |
src/lib/hooks/use-panel-actions.ts (5 sites) |
actions list refresh after CRUD |
src/components/ui/actions/actions-page-container.tsx (5 sites) |
actions list refresh after CRUD |
src/app/coaching-sessions/[id]/page.tsx |
session refresh |
src/components/ui/join-session-popover.tsx |
session list refresh |
src/components/ui/members/member-profile-container.tsx |
members refresh |
src/components/ui/dashboard/coaching-sessions-card.tsx (added in #386) |
sessions list refresh |
src/components/ui/dashboard/dashboard-container.tsx (added in #386) |
sessions list refresh |
SWR dedupes concurrent mutate() calls so leaving these in place wouldn't cause double network requests, but they're a footgun: future contributors will see the manual refresh and wonder if it's load-bearing or vestigial.
Testing
- All existing mutation tests should remain green.
- Add a focused test on
useEntityMutation that asserts both string-keyed and tuple-keyed caches are revalidated on a successful mutation.
- Each call site whose manual
refresh() is removed needs its existing test updated to assert auto-invalidation still drives the same end state.
Why this is a follow-up, not a part of #386
#386 is "add session delete + dashboard UX polish." Bundling a fundamental SWR-mutation refactor:
- doubles the review surface
- mixes unrelated concerns (feature work vs infrastructure)
- forces reviewers to evaluate two different blast radii at once
- requires a wider test sweep than is appropriate for a feature PR
A focused refactor PR is the right shape: predicate fix + cleanup + targeted tests, with its own review and revert story.
Acceptance criteria
Background
useEntityMutation(entity-api.ts:464) auto-invalidates SWR cache entries after a successful mutation by matching keys whose value contains the entity'sbaseUrl:The
typeof key === "string"guard silently skips tuple-keyed caches.useEntityList(entity-api.ts:357) uses tuple keys ([url, params]) so SWR can reuse the same URL with different query params as distinct cache entries:Net effect: every list fetched via
useEntityListis invisible to the auto-invalidate path. After any mutation (create / update / delete), the corresponding lists do not refresh until either a hard reload or a manualrefresh()call.This was surfaced in #386 (the dashboard
CoachingSessionsCarddelete flow) and worked around at the call site, but it affects everyuseEntityListconsumer in the codebase.Proposed fix
Update the predicate to handle both shapes:
Scope
Lists that would newly auto-invalidate (desired effect)
11 tuple-keyed
useEntityListhooks across: agreements, coaching-relationships, coaching-sessions (×2 — relationship-scoped + user-scoped enriched), goal-progress, goals (×2), organizations, organizations/users, user-actions, users.In every case the new behavior is what callers want — the manual
refresh()calls scattered across the codebase exist precisely because this didn't work.Manual
refresh()cleanup (the real work of this issue)After the predicate fix, ~15 manual
refresh()call sites become redundant. Each needs verification that auto-invalidation alone is sufficient (same SWR key, no scope-specific edge cases) before removal:src/components/ui/dashboard/coaching-session-form.tsxsrc/lib/hooks/use-panel-actions.ts(5 sites)src/components/ui/actions/actions-page-container.tsx(5 sites)src/app/coaching-sessions/[id]/page.tsxsrc/components/ui/join-session-popover.tsxsrc/components/ui/members/member-profile-container.tsxsrc/components/ui/dashboard/coaching-sessions-card.tsx(added in #386)src/components/ui/dashboard/dashboard-container.tsx(added in #386)SWR dedupes concurrent
mutate()calls so leaving these in place wouldn't cause double network requests, but they're a footgun: future contributors will see the manual refresh and wonder if it's load-bearing or vestigial.Testing
useEntityMutationthat asserts both string-keyed and tuple-keyed caches are revalidated on a successful mutation.refresh()is removed needs its existing test updated to assert auto-invalidation still drives the same end state.Why this is a follow-up, not a part of #386
#386 is "add session delete + dashboard UX polish." Bundling a fundamental SWR-mutation refactor:
A focused refactor PR is the right shape: predicate fix + cleanup + targeted tests, with its own review and revert story.
Acceptance criteria
useEntityMutationinvalidates tuple-keyeduseEntityListcaches whose URL contains the entity baseUrl.refresh()calls (audited individually) are removed.