mralexsaavedra/spotiarr/spotiarr-zustand

Claude Code and agent skill by mralexsaavedra/spotiarr

Trust tier 3
  • community
Source check pending

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-zustand

SKILL.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. usePreferencesStore and usePlayerStore are the only Zustand stores; playerUISlice is a slice composed into usePlayerStore. 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.
  • useServerEvents invalidates 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 persist middleware with name: "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 persist middleware with name: "spotiarr-player", partialize keeps 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 by GlobalPlayerBar and bound via setAudioElement; 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 like trackUrl or audioUrl to QueueItem.audioUrl) and call playQueue(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 by useServerEvents on 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:
HookReturns
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 TokenGate needs to subscribe to it.
  • Ephemeral by design — "unlocked" must not survive a page reload (a store with persist would 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) not store().
  • Use useShallow (from zustand/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

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
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/mralexsaavedra/spotiarr/spotiarr-zustand/badge)](https://verified-skill.com/skills/mralexsaavedra/spotiarr/spotiarr-zustand)

Something wrong with this skill or its page? Tell us.