Skip to main content

tanstack-query-patterns

Guide for using TanStack Query patterns in the Nora project. Use when implementing data fetching, creating query modules, or consuming IPC data in React components.

الانتقال إلى التثبيت

معلومات المصدر

المستودع
Sandakan/Nora
آخر نشاط في المصدر
٩ أبريل ٢٠٢٦ في ١٧:٥٢
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
١٬٢٢١
التفرعات
٥٨

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
tanstack-query-patterns
description
Guide for using TanStack Query patterns in the Nora project. Use when implementing data fetching, creating query modules, or consuming IPC data in React components.
applyTo
src/renderer/src/**/*.ts*
# TanStack Query Pattern Guide for Nora **Context**: Use this skill when adding new data fetching features, creating query modules, or consuming data from IPC in React components. ## Core Principles 1. **Centralized Query Keys**: All queries are defined in `src/renderer/src/queries/` using `@lukemorales/query-key-factory` 2. **IPC-Driven**: All data flows through `window.api.*` calls to the Electron main process 3. **Type-Safe**: Query keys and functions are strictly typed; cache invalidation keys match data scope 4. **Suspense Ready**: All queries support `useSuspenseQuery` for declarative data loading in routes --- ## File Organization ``` src/renderer/src/queries/ ├── songs.ts # songQuery: all(), allSongInfo(), singleSongInfo(), favorites(), history(), queue(), similarTracks() ├── aritsts.ts # artistQuery: all(), single(), fetchOnlineInfo() ├── albums.ts # albumQuery: all(), single() ├── playlists.ts # playlistQuery: all(), single(), songArtworks() ├── genres.ts # genreQuery: all(), single() ├── home.ts # homeQuery: recentlyPlayedSongs(), recentSongArtists(), mostLovedSongs() ├── listens.ts # listenQuery: single() ├── search.ts # searchQuery: recentResults(), query() ├── lyrics.ts # lyricsQuery (if needed) ├── settings.ts # settingsQuery (with mutations) ├── queue.ts # queueQuery ├── userPreferences.ts # userPreferencesQuery (with mutations) └── other.ts # otherQuery: databaseMetrics() ``` --- ## Creating a Query Module ### Pattern: Simple Queries For straightforward data fetches (e.g., all songs, artist info), create a single `queryKey` without parameters: ```typescript // src/renderer/src/queries/home.ts import { createQueryKeys } from '@lukemorales/query-key-factory'; export const homeQuery = createQueryKeys('home', { recentlyPlayedSongs: { queryKey: null, // No dynamic parameters queryFn: async (): Promise<SongData[]> => { try { const { data: playlists } = await window.api.playlistsData.getPlaylistData([ SpecialPlaylists.History ]); const historyPlaylist = playlists[0]; if (!historyPlaylist || historyPlaylist.songs.length === 0) return []; const songs = await window.api.audioLibraryControls.getSongInfo( historyPlaylist.songs, undefined, undefined, 35, true ); return Array.isArray(songs) ? songs : []; } catch (error) { console.error(error); return []; } } } }); ``` ### Pattern: Parameterized Queries For queries with dynamic parameters (filters, sorting, pagination), accept a `data` object and return both `queryKey` and `queryFn`: ```typescript // src/renderer/src/queries/songs.ts export const songQuery = createQueryKeys('songs', { all: (data: { sortType: SongSortTypes; filterType?: SongFilterTypes; start?: number; end?: number; }) => { const { sortType = 'addedOrder', filterType = 'notSelected', start = 0, end = 0 } = data; return { queryKey: [ `sortType=${sortType}`, `filterType=${filterType}`, `start=${start}`, `end=${end}`, `limit=${end - start}` ], queryFn: () => window.api.audioLibraryControls.getAllSongs(sortType, filterType, { start, end }) }; } }); ``` ### Key Construction Rules **✅ DO**: - Use **template strings** for cache key components: `sortType=${sortType}` - Stabilize arrays before joining in cache keys: `songIds=${[...songIds].sort().join(',')}` - Include all parameters that affect query results - Use semantic names in keys: `start`, `end`, `limit`, `sortType`, `filterType` **❌ DON'T**: - Include object references directly in keys - Use cryptic abbreviations - Mutate the input array when calculating the cache key (copy first) --- ## Using Queries in Components ### Pattern: Route Loader (Pre-fetching) Ensure data is available before component renders: ```typescript // src/renderer/src/routes/main-player/home/index.tsx export const Route = createFileRoute('/main-player/home/')({ component: HomePage, loader: async () => { await queryClient.ensureQueryData( songQuery.all({ sortType: 'dateAddedDescending', start: 0, end: 30 }) ); await queryClient.ensureQueryData(homeQuery.recentlyPlayedSongs); await queryClient.ensureQueryData(homeQuery.recentSongArtists); await queryClient.ensureQueryData(homeQuery.mostLovedSongs); } }); ``` ### Pattern: Component Data Access (Suspense) Use `useSuspenseQuery` for components inside route that already has loader: ```typescript function HomePage() { const { data: latestSongs } = useSuspenseQuery( songQuery.all({ sortType: 'dateAddedDescending', start: 0, end: 30 }) ); const { data: recentlyPlayedSongs } = useSuspenseQuery(homeQuery.recentlyPlayedSongs); // Component renders safely with data return ( <RecentlyPlayedSongs songs={recentlyPlayedSongs.slice(0, 10)} noOfVisibleSongs={10} /> ); } ``` ### Pattern: Conditional Queries For queries that should only run based on conditions, use `enabled` option: ```typescript const { data: artistInfo } = useSuspenseQuery({ ...artistQuery.fetchOnlineInfo({ artistId: selectedArtistId }), enabled: !!selectedArtistId // Only fetch if artistId exists }); ``` --- ## Mutations Store both queries and mutations in the same module: ```typescript // src/renderer/src/queries/settings.ts export const settingsMutation = { toggleAutoLaunch: () => ({ mutationFn: (autoLaunchState: boolean) => window.api.settingsHelpers.toggleAutoLaunch(autoLaunchState), onSuccess: () => { queryClient.invalidateQueries(settingsQuery); } }) }; // Usage in component const { mutate: toggleAutoLaunch } = useMutation(settingsMutation.toggleAutoLaunch()); ``` --- ## Cache Invalidation When data changes via mutations, invalidate affected queries: ```typescript // After toggling song favorite status queryClient.invalidateQueries({ queryKey: songQuery.all.queryKey // Invalidate all song queries }); // After adding to playlist queryClient.invalidateQueries({ queryKey: playlistQuery.all.queryKey // Invalidate playlist list }); ``` --- ## Error Handling All query functions should gracefully handle errors and return safe defaults: ```typescript // ✅ GOOD: Safe error boundary export const homeQuery = createQueryKeys('home', { recentlyPlayedSongs: { queryKey: null, queryFn: async (): Promise<SongData[]> => { try { const { data: playlists } = await window.api.playlistsData.getPlaylistData([ SpecialPlaylists.History ]); // ... process ... return Array.isArray(songs) ? songs : []; // Fallback to empty array } catch (error) { console.error(error); return []; // Return safe default } } } }); // ❌ AVOID: Throwing errors without fallback queryFn: async () => { const { data } = await window.api.playlistsData.getPlaylistData([...]); return data; // Will throw if API fails, breaking component } ``` --- ## Query Key Naming Convention 1. **Query Module Name** (namespace): `songs`, `artists`, `home`, `playlists` 2. **Query Function Name** (descriptor): `all`, `single`, `favorites`, `recentlyPlayedSongs` 3. **Parameters** (dynamic cache keys): `sortType=${sortType}`, `songIds=${[...].sort().join(',')}` Example: ``` homeQuery.recentlyPlayedSongs > Module: 'home' > Key: null (no params) > Cache: ['home', 'recentlyPlayedSongs'] songQuery.all({ sortType: 'aToZ', start: 0, end: 30 }) > Module: 'songs' > Key: 'all', 'sortType=aToZ', 'start=0', 'end=30', 'limit=30' > Cache: ['songs', 'all', 'sortType=aToZ', 'start=0', 'end=30', 'limit=30'] ``` --- ## Common Patterns ### Pattern: Derived Query (Artist extraction from recently played songs) When one query depends on another: ```typescript const fetchRecentSongArtists = async (): Promise<Artist[]> => { try { // 1. Ensure recently played songs are cached const recentlyPlayedSongs = await queryClient.ensureQueryData(homeQuery.recentlyPlayedSongs); if (recentlyPlayedSongs.length === 0) return []; // 2. Extract artist IDs from songs const artistIds = [ ...new Set( recentlyPlayedSongs .map((song) => song.artists?.map((artist) => artist.artistId) ?? []) .flat() ) ]; if (artistIds.length === 0) return []; // 3. Fetch artist data via IPC const { data: artists } = await window.api.artistsData.getArtistData( artistIds, undefined, undefined, 0, 35 ); return artists; } catch (error) { console.error(error); return []; } }; ``` ### Pattern: Array Stabilization for Cache Keys When query accepts an array parameter (e.g., song IDs), sort before building cache key: ```typescript allSongInfo: (data: { songIds: number[] }) => { const { songIds } = data; return { queryKey: [ // Sort to ensure cache key stability (always same regardless of input order) `songIds=${[...songIds].sort().join(',')}` ], queryFn: () => window.api.audioLibraryControls.getSongInfo(songIds) }; }; ``` --- ## Best Practices 1. **Keep query functions pure**: No side effects outside try/catch 2. **Return type-safe defaults**: Empty arrays, null, or sentinel values — never throw from queryFn 3. **Batch related queries**: If songs and artists are always fetched together, consider a combined query 4. **Use queryClient.ensureQueryData** in route loaders to pre-cache before render 5. **Centralize in `src/renderer/src/queries/`**: Never define queries inline in components 6. **Document query scope**: Add comments for complex queries about their IPC calls and dependencies 7. **Test error paths**: Queries should remain stable even if IPC fails --- ## Debugging ### Query Client DevTools Install `@tanstack/react-query-devtools` to inspect cache: ```tsx import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; export default App() { return ( <> {/* Your app */} <ReactQueryDevtools initialIsOpen={false} /> </> ); } ``` ### Cache Inspection ```typescript // Log current cache state console.log(queryClient.getQueryData(songQuery.all({ sortType: 'aToZ' }).queryKey)); // Manually invalidate and refetch queryClient.invalidateQueries({ queryKey: ['songs'] }); ``` --- ## Related Files - **Query Modules**: `src/renderer/src/queries/*.ts` - **IPC Bridge**: `src/preload/index.ts` (defines `window.api` interface) - **Usage Example**: `src/renderer/src/routes/main-player/home/index.tsx` - **Query Client Setup**: `src/renderer/src/index.tsx` (queryClient initialization)
عرض على GitHub