Audio Store

PreviousNext

Zustand-based store and helper actions for queue and playback management.

Installation

Dependencies

The store uses Zustand. Install it if your app doesn't already include it:

pnpm add zustand

Import

Import the store hook and types:

import {
  useAudioStore,
  calculateNextIndex,
  calculatePreviousIndex,
  canUseDOM,
  type AudioStore,
  type RepeatMode,
  type InsertMode,
} from "@/lib/audio-store"

Core Concepts

useAudioStore Hook

Performance Best Practices: Use granular selectors for better performance. Subscribe only to the specific slices of state you need. This prevents unnecessary re-renders when unrelated state changes.

Access the store with granular selectors for better performance:

const currentTime = useAudioStore((s) => s.currentTime)
const isPlaying = useAudioStore((s) => s.isPlaying)

For multiple values, extract them inside your component:

function PlayerStatus() {
  const currentTrack = useAudioStore((s) => s.currentTrack)
  const duration = useAudioStore((s) => s.duration)
  const isPlaying = useAudioStore((s) => s.isPlaying)
  
  return (
    <p>
      {currentTrack?.title} ({duration}s) — {isPlaying ? "▶" : "⏸"}
    </p>
  )
}

Architecture

Types

Track

Common audio track object. See Audio library for full definition.

AudioStore State

The store exposes state organized by concern:

CategoryPropertyTypeDescription
PlaybackisPlayingbooleanCurrently playing
isLoadingbooleanLoading track
isBufferingbooleanBuffering audio
isErrorbooleanError state
errorMessagestring | nullError details
Current TrackcurrentTrackTrack | nullActive track object
currentTimenumberPlayback position (seconds)
durationnumberTrack length
progressnumberNormalized progress 0–1
bufferedTimenumberBuffered amount
QueuequeueTrack[]Array of tracks
historyTrack[]Previously played tracks
currentQueueIndexnumberActive track index
ControlsvolumenumberVolume 0–1
isMutedbooleanMute state
repeatMode"none" | "one" | "all"Repeat mode
shuffleEnabledbooleanShuffle state
insertMode"first" | "last" | "after"Insertion position for queue

Types

TypeValuesDescription
RepeatMode"none" | "one" | "all"Repeat playback mode
InsertMode"first" | "last" | "after"Where new tracks are added to the queue

RepeatMode Flow

Utility Functions

calculateNextIndex(queue, currentIndex, shuffleEnabled, repeatMode)

Calculate the next track index based on playback mode.

const nextIndex = calculateNextIndex(
  queue,
  currentQueueIndex,
  shuffleEnabled,
  repeatMode
)
// Returns: number (track index or -1 if none)

calculatePreviousIndex(queue, currentIndex, shuffleEnabled, repeatMode)

Calculate the previous track index based on playback mode.

const prevIndex = calculatePreviousIndex(
  queue,
  currentQueueIndex,
  shuffleEnabled,
  repeatMode
)
// Returns: number (track index or -1 if none)

canUseDOM()

Check if code runs in a DOM environment (not SSR).

if (canUseDOM()) {
  // Browser-only code
}

SSR Considerations: The store uses canUseDOM() internally to handle server-side rendering. When accessing the store in SSR contexts, ensure you check for DOM availability before calling store methods that interact with browser APIs.

Actions

Access actions via selectors: const action = useAudioStore((s) => s.actionName)

CategoryActionSignatureDescription
Playbackplay() => Promise<void>Start or resume playback
pause() => voidPause playback
togglePlay() => voidToggle play/pause state
seek(time: number) => voidSeek to position (seconds)
Navigationnext() => Promise<void>Play next track
previous() => Promise<void>Play previous track
setCurrentTrack(track: Track | null) => Promise<void>Load and play specific track
setQueueAndPlay(tracks: Track[], startIndex: number) => Promise<void>Set queue and play from index
QueueaddToQueue(track: Track, mode?: InsertMode) => voidAdd track to queue (supports "first", "last", "after")
removeFromQueue(trackId: string) => voidRemove track from queue
moveInQueue(fromIndex: number, toIndex: number) => voidMove track in queue
setQueue(tracks: Track[], startIndex?: number) => voidReplace entire queue
clearQueue() => voidClear all tracks from queue
VolumesetVolume(volume: number) => voidSet volume (0-1)
toggleMute() => voidToggle mute state
ModeschangeRepeatMode() => voidCycle repeat mode (none → one → all → none)
setRepeatMode(mode: RepeatMode) => voidSet repeat mode
shuffle() => voidRandomize queue order
unshuffle() => voidRestore original queue order
setInsertMode(mode: InsertMode) => voidSet insert mode
ErrorsetError(message: string | null) => voidSet or clear error state

Examples

Basic Usage

import { useAudioStore } from "@/lib/audio-store"
import { formatDuration } from "@/lib/audio"
 
function PlayerStatus() {
  // Access state with selectors
  const currentTrack = useAudioStore((s) => s.currentTrack)
  const currentTime = useAudioStore((s) => s.currentTime)
  const duration = useAudioStore((s) => s.duration)
  const isPlaying = useAudioStore((s) => s.isPlaying)
  
  // Access actions
  const togglePlay = useAudioStore((s) => s.togglePlay)
  const next = useAudioStore((s) => s.next)
  const previous = useAudioStore((s) => s.previous)
 
  return (
    <div>
      <p>Track: {currentTrack?.title ?? "None"}</p>
      <p>Time: {formatDuration(currentTime)} / {formatDuration(duration)}</p>
      <p>Status: {isPlaying ? "▶" : "⏸"}</p>
      <div className="flex gap-2">
        <button onClick={previous}>◀ Prev</button>
        <button onClick={togglePlay}>{isPlaying ? "⏸" : "▶"}</button>
        <button onClick={next}>Next ▶</button>
      </div>
    </div>
  )
}

Queue Management

function QueueManager() {
  const queue = useAudioStore((s) => s.queue)
  const addToQueue = useAudioStore((s) => s.addToQueue)
  const removeFromQueue = useAudioStore((s) => s.removeFromQueue)
  const clearQueue = useAudioStore((s) => s.clearQueue)
 
  return (
    <div>
      <div className="flex gap-2 mb-4">
        <button onClick={() => addToQueue(newTrack, "last")}>Add Track</button>
        <button onClick={clearQueue}>Clear</button>
      </div>
      <div className="space-y-1">
        {queue.map((track) => (
          <div key={track.id} className="flex justify-between">
            <span>{track.title}</span>
            <button onClick={() => removeFromQueue(track.id)}>Remove</button>
          </div>
        ))}
      </div>
    </div>
  )
}

Direct Store Access

import { useAudioStore } from "@/lib/audio-store"
 
// Read state without React subscriptions
const state = useAudioStore.getState()
console.log(state.isPlaying)
 
// Call actions directly
await useAudioStore.getState().play()
 
// Subscribe to changes (returns unsubscribe function)
const unsubscribe = useAudioStore.subscribe(
  (s) => s.isPlaying,
  (isPlaying) => console.log("Playing:", isPlaying)
)

Persistence

The store automatically persists a subset of state to localStorage using Zustand's persist middleware:

CategoryProperties
PlaybackcurrentTrack, currentTime, currentQueueIndex
Queuequeue, history
Settingsvolume, isMuted, repeatMode, shuffleEnabled, insertMode

Storage Key: audio:ui:store

This allows users to resume playback and maintain queue state across page refreshes.

Notes

Best Practices:
  • Prefer selector subscriptions for performance (subscribe to specific slices of state)
  • Async actions (play, next, setCurrentTrack) wait for audio loading
  • addToQueue supports "first", "last", and "after" insert modes
  • Queue shuffling randomizes order but preserves track identity
  • setQueueAndPlay and similar actions trigger audio loading via the Audio library
  • Persistence is automatic; no manual save required
  • Direct store access via getState() is useful for non-React code or imperative operations

Last updated 11/18/2025