refactor(fe): query-key factory + migrate feed/channels/playlists (R7 S2a)

Introduce lib/queryKeys.ts as the single source of truth for React Query
keys — the R7 driver was ~200 hand-written key literals across 51 files
(`["feed"]` alone in 15+), where one typo silently orphans a key so an
invalidate never matches.

S2a migrates the three highest-traffic domains (feed/video, channels,
playlists) plus the shared me/my-status/tags roots: ~90 literal sites in
18 files now call qk.*(). Pure substitution — each factory fn returns the
byte-identical array the call sites used before, so TanStack's prefix
matching is unchanged (qk.feed() still invalidates qk.feed(filters)).
Co-invalidation clusters are deliberately NOT bundled into helpers (they
differ per site), so no query's refetch scope changes.

Nullable keys (playlist(selectedId), ytSearch(q)) accept null so a null
segment is preserved (["playlist", null]) while a no-arg call is the bare
prefix key — guard is `!== undefined`.

Gate green: typecheck, eslint (0 err), prettier, 68 vitest. Smoke: app
boots, feed/facets/tags/my-status all 200, no console errors. Factory
extended to plex/messaging/admin/downloads/notifications in S2b.
This commit is contained in:
2026-07-27 21:48:35 +02:00
parent d0aa1cf9a7
commit ad86d6d126
19 changed files with 169 additions and 107 deletions
+6 -5
View File
@@ -1,6 +1,7 @@
import { useEffect, useRef } from "react";
import { Trans, useTranslation } from "react-i18next";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { qk } from "../lib/queryKeys";
import { AlertTriangle, Check, Loader2, RefreshCw, ShieldCheck, X, Youtube } from "lucide-react";
import { api, type Me } from "../lib/api";
import { beginGrant, endOnboarding } from "../lib/onboarding";
@@ -63,7 +64,7 @@ export default function OnboardingWizard({ me, onClose }: { me: Me; onClose: ()
// Once we have read access, find out whether any subscriptions are imported yet.
const myStatus = useQuery({
queryKey: ["my-status"],
queryKey: qk.myStatus(),
queryFn: api.myStatus,
enabled: me.can_read,
});
@@ -72,10 +73,10 @@ export default function OnboardingWizard({ me, onClose }: { me: Me; onClose: ()
mutationFn: () => api.syncSubscriptions(),
onSuccess: () => {
// Feed/channels/status now have content — refresh the app behind the wizard.
qc.invalidateQueries({ queryKey: ["feed"] });
qc.invalidateQueries({ queryKey: ["feed-count"] });
qc.invalidateQueries({ queryKey: ["channels"] });
qc.invalidateQueries({ queryKey: ["my-status"] });
qc.invalidateQueries({ queryKey: qk.feed() });
qc.invalidateQueries({ queryKey: qk.feedCount() });
qc.invalidateQueries({ queryKey: qk.channels() });
qc.invalidateQueries({ queryKey: qk.myStatus() });
},
});