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
ソースの最終更新活動
2026年4月9日 17:52
検出された SKILL.md の言語
英語
スター
1,221
フォーク
58

インストール方法

デフォルトでは、最初にソースを確認する 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で見る