mralexsaavedra/spotiarr/spotiarr-zustand
Claude Code and agent skill by mralexsaavedra/spotiarr
- community
Checking that the SKILL.md file is still in the repository.
Trigger: Zustand, store, client state, usePlayerStore, usePreferencesStore, playerUISlice, ephemeral state, persist middleware. spotiarr Zustand rules: 2 stores + player UI slice, server state stays in TanStack Query, patterns for selectors and bulk hooks.
Install
npx vskill@latest install mralexsaavedra/spotiarr/spotiarr-zustandSKILL.md
Activation Contract
Load when adding client-side state, touching Zustand stores, or deciding whether something belongs in Zustand vs TanStack Query.
Hard Rules
- Two stores + one player UI slice.
usePreferencesStoreandusePlayerStoreare the only Zustand stores;playerUISliceis a slice composed intousePlayerStore. Do not add a new store without an architectural decision recorded in an SDD proposal. - Server state → TanStack Query. UI/client-only state → Zustand. Never mix.
- Download status is not a Zustand store — it is TanStack Query state (see below). This is the canonical example of the rule above.
useServerEventsinvalidates TanStack Query caches — it does NOT write to Zustand stores.- No inline store creation inside components. Stores are singletons in
apps/frontend/src/store/.
The Stores
usePreferencesStore — persisted
- File:
apps/frontend/src/store/usePreferencesStore.ts - Uses
persistmiddleware withname: "spotiarr-preferences"(localStorage key). - Holds durable UI preferences that survive page refresh.
- Current state:
isSidebarCollapsed,toggleSidebar,setSidebarCollapsed.
usePlayerStore — partially persisted
- File:
apps/frontend/src/store/usePlayerStore.ts - Uses
persistmiddleware withname: "spotiarr-player",partializekeeps only{ volume, isMuted }. - Holds global audio playback state:
queue,currentIndex,isPlaying,currentTime,duration,error, plus volume/mute. - Composes
playerUISlice(apps/frontend/src/store/playerUISlice.ts) for player UI state (e.g. fullscreen/queue panel visibility). - The single
<audio>element is owned byGlobalPlayerBarand bound viasetAudioElement; internal handlers (_onTimeUpdate,_onEnded, etc.) are wired by the bar and not called from controllers. - Dispatch pattern: page controllers build a
QueueItem[]snapshot from their domain data (normalizing source DTO fields liketrackUrloraudioUrltoQueueItem.audioUrl) and callplayQueue(items, startIndex). - Shared binding hook: use
usePlayerQueueBinding(queueItems)(apps/frontend/src/hooks/usePlayerQueueBinding.ts) to read{ currentTrackId, isPlaying, hasPlayableTracks, playFromIndex, onPlayTrack, onPauseTrack }. Do not subscribe to the same selectors manually in new controllers.
Download Status — TanStack Query, not a store
Download status used to be a Zustand store; it is now server state served by TanStack Query.
- Query hook:
useDownloadStatusQuery(apps/frontend/src/hooks/queries/useDownloadStatusQuery.ts) —queryKey: queryKeys.downloadStatus,queryFn: playlistService.getDownloadStatus(),staleTime: Infinity. Invalidated byuseServerEventson download events. - Response shape (
DownloadStatusResponse):playlistStatusMap,albumTrackCountMap,trackStatusMap. - Derived selector hooks (
apps/frontend/src/hooks/queries/useDownloadStatus.ts) — prefer these over reading the query data directly:
| Hook | Returns | |
|---|---|---|
usePlaylistDownloaded(url, expectedTrackCount?) | boolean | |
usePlaylistDownloading(url) | boolean | |
useBulkPlaylistStatus(items) | Map<string, { isDownloaded, isDownloading }> | |
useBulkTrackStatus(urls) | Map<string, TrackStatusEnum> |
Pure derivers deriveIsDownloaded / deriveIsDownloading back the hooks and are unit-testable in isolation. Use bulk hooks in list components to avoid per-item subscriptions.
Auth gate — ephemeral React state (not a store)
useTokenGate (apps/frontend/src/hooks/controllers/useTokenGate.ts) deliberately uses local useState, not a Zustand store:
- One-time startup concern; no component outside
TokenGateneeds to subscribe to it. - Ephemeral by design — "unlocked" must not survive a page reload (a store with
persistwould break this).
Promoting it to a store requires a written SDD proposal.
Adding State
To `usePreferencesStore`: Add field to PreferencesState interface + initial value + setter/toggle. No extra config — persist covers all fields automatically.
To `usePlayerStore`: Add field to the state interface + initial value + action; if it is UI-only player state, add it to playerUISlice. Extend partialize only when the field must survive a reload.
Download-status-shaped state: Add it to the DownloadStatusResponse contract and a derived selector hook in hooks/queries/useDownloadStatus.ts — do not put it in a store.
New store (discouraged): Requires architectural justification recorded in an SDD proposal. Mixed strategy (partial persist via partialize) is permitted when the store covers state with different lifetimes — see usePlayerStore for the canonical example.
Selector Performance
- Use granular selectors:
store((state) => state.specificField)notstore(). - Use
useShallow(fromzustand/react/shallow) for selectors returning objects or arrays. - For list components reading N items: use bulk hooks (
useBulkPlaylistStatus,useBulkTrackStatus) to batch subscriptions.
References
- Stores:
apps/frontend/src/store/(usePlayerStore.ts,usePreferencesStore.ts,playerUISlice.ts) - Download status (TanStack Query):
apps/frontend/src/hooks/queries/useDownloadStatus.ts,useDownloadStatusQuery.ts - Player binding hook:
apps/frontend/src/hooks/usePlayerQueueBinding.ts - Global player bar:
apps/frontend/src/components/organisms/GlobalPlayerBar.tsx - Shared enums:
packages/shared/src/index.ts—PlaylistStatusEnum,TrackStatusEnum
Security
- Security scanPassed, 95 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingskills/spotiarr-zustand/SKILL.md
Popularity
- Repository stars
- 35
- Repository forks
- 6
- 7-day trend score
- 18
Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/mralexsaavedra/spotiarr/spotiarr-zustand)