From 30085ab3e1f11ae36edd5839e09202f604c59d74 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 30 Jun 2026 06:58:41 +0000 Subject: [PATCH] Skip settings loader revalidation on section switches Section navigation only changes the :section path param, but the loader returns the same user/draft-status/linked-account payload for every section. By default React Router re-runs the loader (3 DB queries) on each tab switch. Add shouldRevalidate to skip revalidation when only the section changes, while still refreshing after mutations. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Jt8Hhsio4bHGMaDZy7ftBs --- app/routes/__tests__/settings.test.ts | 48 ++++++++++++++++++++++++++- app/routes/settings.tsx | 16 ++++++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/app/routes/__tests__/settings.test.ts b/app/routes/__tests__/settings.test.ts index da286d2..660dcca 100644 --- a/app/routes/__tests__/settings.test.ts +++ b/app/routes/__tests__/settings.test.ts @@ -22,9 +22,21 @@ vi.mock("~/models/account", () => ({ findLinkedAccountsByUserId: vi.fn() })); vi.mock("~/models/team", () => ({ findTeamsByOwnerId: vi.fn(), removeTeamOwner: vi.fn() })); vi.mock("~/models/commissioner", () => ({ removeAllCommissionersByUserId: vi.fn() })); -import { loader } from "../settings"; +import { loader, shouldRevalidate } from "../settings"; import { auth } from "~/lib/auth.server"; +type RevalidateArgs = Parameters[0]; + +function revalidateArgs(overrides: Partial): RevalidateArgs { + return { + currentParams: {}, + nextParams: {}, + formMethod: undefined, + defaultShouldRevalidate: true, + ...overrides, + } as unknown as RevalidateArgs; +} + type LoaderArgs = Parameters[0]; function loaderArgs(section?: string): LoaderArgs { @@ -54,3 +66,37 @@ describe("/settings loader section handling", () => { expect(auth.api.getSession).toHaveBeenCalled(); }); }); + +describe("/settings shouldRevalidate", () => { + it("skips revalidation when only the section param changes", () => { + expect( + shouldRevalidate( + revalidateArgs({ currentParams: { section: "profile" }, nextParams: { section: "account" } }) + ) + ).toBe(false); + }); + + it("revalidates after a mutation regardless of section", () => { + expect( + shouldRevalidate( + revalidateArgs({ + currentParams: { section: "profile" }, + nextParams: { section: "account" }, + formMethod: "POST", + }) + ) + ).toBe(true); + }); + + it("defers to the default when the section is unchanged", () => { + expect( + shouldRevalidate( + revalidateArgs({ + currentParams: { section: "account" }, + nextParams: { section: "account" }, + defaultShouldRevalidate: true, + }) + ) + ).toBe(true); + }); +}); diff --git a/app/routes/settings.tsx b/app/routes/settings.tsx index d680b65..99cebd1 100644 --- a/app/routes/settings.tsx +++ b/app/routes/settings.tsx @@ -1,4 +1,4 @@ -import { redirect } from "react-router"; +import { redirect, type ShouldRevalidateFunctionArgs } from "react-router"; import { Bell, Key, Lock, Shield, User } from "lucide-react"; import { auth } from "~/lib/auth.server"; import { findUserById, findUserByUsername, isUserInActiveDraft, updateUser, anonymizeUserAccount, USERNAME_RE } from "~/models/user"; @@ -56,6 +56,20 @@ export function meta(): Route.MetaDescriptors { return [{ title: "Settings - Brackt" }]; } +export function shouldRevalidate({ + currentParams, + nextParams, + formMethod, + defaultShouldRevalidate, +}: ShouldRevalidateFunctionArgs) { + // Mutations (profile/avatar/notification updates) must refresh loader data. + if (formMethod && formMethod !== "GET") return true; + // Switching sections only changes the path param, and the loader payload is the + // same for every section, so don't refetch user/draft status/linked accounts. + if (currentParams.section !== nextParams.section) return false; + return defaultShouldRevalidate; +} + export async function loader(args: Route.LoaderArgs) { const section = args.params.section; if (section && !(VALID_SECTION_IDS as Set).has(section)) {