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:
@@ -1,6 +1,7 @@
|
||||
import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import {
|
||||
DndContext,
|
||||
KeyboardSensor,
|
||||
@@ -171,8 +172,8 @@ export default function Playlists() {
|
||||
// Refresh the sidebar (cover may change) and the detail (so the now-dirty state,
|
||||
// and the Reset/Unsynced indicators, show without an F5). The membership guard
|
||||
// keeps the refetch from wiping the undo history on a pure reorder.
|
||||
qc.invalidateQueries({ queryKey: ["playlists"] });
|
||||
qc.invalidateQueries({ queryKey: ["playlist", selectedId] });
|
||||
qc.invalidateQueries({ queryKey: qk.playlists() });
|
||||
qc.invalidateQueries({ queryKey: qk.playlist(selectedId) });
|
||||
})
|
||||
.catch((e) => {
|
||||
// Was a bare .then(): a failed reorder left the UI showing an order the server had
|
||||
@@ -182,7 +183,7 @@ export default function Playlists() {
|
||||
// the arriving detail is judged "same list" and the rejected order stays on screen — and
|
||||
// becomes the base for the next drag.
|
||||
lastSetRef.current = "";
|
||||
qc.invalidateQueries({ queryKey: ["playlist", selectedId] });
|
||||
qc.invalidateQueries({ queryKey: qk.playlist(selectedId) });
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -201,14 +202,14 @@ export default function Playlists() {
|
||||
const [playingId, setPlayingId] = useState<string | null>(null);
|
||||
|
||||
const listQuery = useQuery({
|
||||
queryKey: ["playlists"],
|
||||
queryKey: qk.playlists(),
|
||||
queryFn: () => api.playlists(),
|
||||
refetchOnWindowFocus: true,
|
||||
});
|
||||
const playlists = listQuery.data ?? [];
|
||||
|
||||
const detailQuery = useQuery({
|
||||
queryKey: ["playlist", selectedId],
|
||||
queryKey: qk.playlist(selectedId),
|
||||
queryFn: () => api.playlist(selectedId as number),
|
||||
enabled: selectedId != null,
|
||||
// Refetch when returning to the tab so a change made elsewhere (another tab/device)
|
||||
@@ -331,8 +332,8 @@ export default function Playlists() {
|
||||
const plName = (p: { kind: string; name: string }) => playlistName(p, t);
|
||||
|
||||
function refreshAll() {
|
||||
qc.invalidateQueries({ queryKey: ["playlists"] });
|
||||
qc.invalidateQueries({ queryKey: ["playlist", selectedId] });
|
||||
qc.invalidateQueries({ queryKey: qk.playlists() });
|
||||
qc.invalidateQueries({ queryKey: qk.playlist(selectedId) });
|
||||
}
|
||||
|
||||
async function revertToYoutube() {
|
||||
@@ -499,14 +500,14 @@ export default function Playlists() {
|
||||
(onYoutube ? t("playlists.deleteYtFailed") : t("playlists.deleteFailed")),
|
||||
});
|
||||
}
|
||||
qc.removeQueries({ queryKey: ["playlist", deletedId] });
|
||||
qc.invalidateQueries({ queryKey: ["playlists"] });
|
||||
qc.removeQueries({ queryKey: qk.playlist(deletedId) });
|
||||
qc.invalidateQueries({ queryKey: qk.playlists() });
|
||||
}
|
||||
|
||||
function playerState(id: string, status: string) {
|
||||
api.setState(id, status).then(() => {
|
||||
qc.invalidateQueries({ queryKey: ["playlist", selectedId] });
|
||||
qc.invalidateQueries({ queryKey: ["feed"] });
|
||||
qc.invalidateQueries({ queryKey: qk.playlist(selectedId) });
|
||||
qc.invalidateQueries({ queryKey: qk.feed() });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user