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:
@@ -6,6 +6,7 @@ import {
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
@@ -155,7 +156,7 @@ export default function Feed({
|
||||
const queryFilters: FeedFilters = { ...filters, q: debouncedQ };
|
||||
|
||||
const query = useInfiniteQuery({
|
||||
queryKey: ["feed", queryFilters],
|
||||
queryKey: qk.feed(queryFilters),
|
||||
queryFn: ({ pageParam }) => api.feed(queryFilters, pageParam as string | null, PAGE),
|
||||
initialPageParam: null as string | null,
|
||||
getNextPageParam: (last) => last.next_cursor ?? undefined,
|
||||
@@ -168,7 +169,7 @@ export default function Feed({
|
||||
// retry:false so a 429 (quota/limit) surfaces at once for inline display. staleTime keeps a
|
||||
// revisited search from re-spending.
|
||||
const ytQuery = useInfiniteQuery({
|
||||
queryKey: ["yt-search", ytSearch, ytCount],
|
||||
queryKey: qk.ytSearch(ytSearch, ytCount),
|
||||
queryFn: ({ pageParam }) =>
|
||||
api.searchYoutube(ytSearch as string, pageParam as string | null, ytCount),
|
||||
initialPageParam: null as string | null,
|
||||
@@ -225,7 +226,7 @@ export default function Feed({
|
||||
}, [query.dataUpdatedAt]);
|
||||
|
||||
const countQuery = useQuery({
|
||||
queryKey: ["feed-count", queryFilters],
|
||||
queryKey: qk.feedCount(queryFilters),
|
||||
queryFn: () => api.feedCount(queryFilters),
|
||||
staleTime: 30_000,
|
||||
enabled: !ytActive,
|
||||
@@ -250,7 +251,7 @@ export default function Feed({
|
||||
api
|
||||
.setState(id, status)
|
||||
.then(() => {
|
||||
qc.invalidateQueries({ queryKey: ["feed"] });
|
||||
qc.invalidateQueries({ queryKey: qk.feed() });
|
||||
if (status === "hidden") {
|
||||
const v = loadedRef.current.find((x) => x.id === id);
|
||||
notify({
|
||||
@@ -307,7 +308,7 @@ export default function Feed({
|
||||
setOverrides((o) => ({ ...o, [id]: "new" }));
|
||||
api
|
||||
.clearState(id)
|
||||
.then(() => qc.invalidateQueries({ queryKey: ["feed"] }))
|
||||
.then(() => qc.invalidateQueries({ queryKey: qk.feed() }))
|
||||
.catch(() =>
|
||||
setOverrides((o) => {
|
||||
const next = { ...o };
|
||||
@@ -324,8 +325,8 @@ export default function Feed({
|
||||
setSavedOverrides((o) => ({ ...o, [id]: saved }));
|
||||
(saved ? api.watchLaterAdd(id) : api.watchLaterRemove(id))
|
||||
.then(() => {
|
||||
qc.invalidateQueries({ queryKey: ["playlists"] });
|
||||
qc.invalidateQueries({ queryKey: ["playlist"] });
|
||||
qc.invalidateQueries({ queryKey: qk.playlists() });
|
||||
qc.invalidateQueries({ queryKey: qk.playlist() });
|
||||
})
|
||||
.catch(() => setSavedOverrides((o) => ({ ...o, [id]: !saved })));
|
||||
},
|
||||
@@ -355,9 +356,9 @@ export default function Feed({
|
||||
// A live search ingests new catalog videos, so the disabled feed queries still hold their stale
|
||||
// pre-search cache. Drop them so the feed re-fetches fresh on return.
|
||||
const dropFeedCaches = () => {
|
||||
qc.removeQueries({ queryKey: ["feed"] });
|
||||
qc.removeQueries({ queryKey: ["feed-count"] });
|
||||
qc.removeQueries({ queryKey: ["facets"] });
|
||||
qc.removeQueries({ queryKey: qk.feed() });
|
||||
qc.removeQueries({ queryKey: qk.feedCount() });
|
||||
qc.removeQueries({ queryKey: qk.facets() });
|
||||
};
|
||||
|
||||
// --- Live YouTube search mode -------------------------------------------------------------
|
||||
@@ -435,7 +436,7 @@ export default function Feed({
|
||||
setFilters({ ...filters, librarySource: "organic", sort: "newest" });
|
||||
onExitYtSearch();
|
||||
dropFeedCaches();
|
||||
qc.removeQueries({ queryKey: ["yt-search"] });
|
||||
qc.removeQueries({ queryKey: qk.ytSearch() });
|
||||
}}
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full border border-border text-muted hover:text-danger hover:border-danger transition"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user