Skip to content

Refactor: useEntityMutation should invalidate tuple-keyed useEntityList caches #387

Description

@jhodapp

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

  • useEntityMutation invalidates tuple-keyed useEntityList caches whose URL contains the entity baseUrl.
  • All redundant manual refresh() calls (audited individually) are removed.
  • A unit test pins the predicate's behavior on both key shapes.
  • Full test suite passes; manual smoke of one mutation path per affected entity.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementImproves existing functionality or feature

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions