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 { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { useNavigationActions } from "./NavigationProvider";
|
||||
import {
|
||||
ArrowLeft,
|
||||
@@ -73,7 +74,7 @@ export default function ChannelPage({
|
||||
const [bannerAttempt, setBannerAttempt] = useState(0);
|
||||
|
||||
const detail = useQuery({
|
||||
queryKey: ["channel", channelId],
|
||||
queryKey: qk.channel(channelId),
|
||||
queryFn: () => api.channelDetail(channelId),
|
||||
});
|
||||
const ch = detail.data;
|
||||
@@ -91,10 +92,10 @@ export default function ChannelPage({
|
||||
.exploreChannel(channelId, token ?? undefined)
|
||||
.then((r) => {
|
||||
setNextToken(r.next_page_token);
|
||||
qc.invalidateQueries({ queryKey: ["feed"] });
|
||||
qc.invalidateQueries({ queryKey: qk.feed() });
|
||||
// Refetch the detail so `explored` flips true → the "Exploring" badge shows on the
|
||||
// first visit (the GET that drove this render predated the explore that just ran).
|
||||
qc.invalidateQueries({ queryKey: ["channel", channelId] });
|
||||
qc.invalidateQueries({ queryKey: qk.channel(channelId) });
|
||||
})
|
||||
.finally(() => setExploring(false));
|
||||
},
|
||||
@@ -116,9 +117,9 @@ export default function ChannelPage({
|
||||
const block = useMutation({
|
||||
mutationFn: () => (ch?.blocked ? api.unblockChannel(channelId) : api.blockChannel(channelId)),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["channel", channelId] });
|
||||
qc.invalidateQueries({ queryKey: ["feed"] });
|
||||
qc.invalidateQueries({ queryKey: ["blocked-channels"] });
|
||||
qc.invalidateQueries({ queryKey: qk.channel(channelId) });
|
||||
qc.invalidateQueries({ queryKey: qk.feed() });
|
||||
qc.invalidateQueries({ queryKey: qk.blockedChannels() });
|
||||
},
|
||||
// No onError: block/unblock is a local DB op (not a YouTube write, so no 403 "connect" case),
|
||||
// and its 400/409/422/500 are already surfaced by the global error modal (see api.ts req) —
|
||||
@@ -128,12 +129,12 @@ export default function ChannelPage({
|
||||
const subscribe = useMutation({
|
||||
mutationFn: () => api.subscribeChannel(channelId),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["channel", channelId] });
|
||||
qc.invalidateQueries({ queryKey: ["feed"] });
|
||||
qc.invalidateQueries({ queryKey: ["channels"] });
|
||||
qc.invalidateQueries({ queryKey: qk.channel(channelId) });
|
||||
qc.invalidateQueries({ queryKey: qk.feed() });
|
||||
qc.invalidateQueries({ queryKey: qk.channels() });
|
||||
// Match ChannelDiscovery: the channel leaves discovery and the manager's stats move.
|
||||
qc.invalidateQueries({ queryKey: ["my-status"] });
|
||||
qc.invalidateQueries({ queryKey: ["discovered-channels"] });
|
||||
qc.invalidateQueries({ queryKey: qk.myStatus() });
|
||||
qc.invalidateQueries({ queryKey: qk.discoveredChannels() });
|
||||
notify({ level: "info", message: t("channel.subscribed", { name: ch?.title ?? "" }) });
|
||||
},
|
||||
// A 403 (missing write scope) otherwise fails silently on the channel page (no wizard handle
|
||||
@@ -143,9 +144,9 @@ export default function ChannelPage({
|
||||
const unsubscribe = useMutation({
|
||||
mutationFn: () => api.unsubscribeChannel(channelId),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["channel", channelId] });
|
||||
qc.invalidateQueries({ queryKey: ["feed"] });
|
||||
qc.invalidateQueries({ queryKey: ["channels"] });
|
||||
qc.invalidateQueries({ queryKey: qk.channel(channelId) });
|
||||
qc.invalidateQueries({ queryKey: qk.feed() });
|
||||
qc.invalidateQueries({ queryKey: qk.channels() });
|
||||
},
|
||||
onError: (err) => notifyYouTubeActionError(err, t("channels.notify.unsubscribeFailed")),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user