Korai Docs
Playlist

Playlist Analyzer Store

Detailed documentation of the Zustand store for playlist analysis state management

Overview

The Playlist Analyzer store (analyze-store.ts) uses Zustand for global state management. It provides a centralized, type-safe way to manage playlist analysis state across the entire feature.

Store Location

/src/features/analyze/store/analyze-store.ts

Complete Implementation

import { create } from 'zustand';
import type { PlaylistDetails, VideoItem } from '@/lib/youtube';

interface PlaylistData {
  playlistDetails: PlaylistDetails;
  videos: VideoItem[];
  totalDuration: number;
  totalVideos: number;
}

interface AnalyzeState {
  playlistUrl: string;
  playlistData: PlaylistData | null;
  rangeStart: string;
  rangeEnd: string;
  sortBy: string;
  playbackSpeed: string;
  searchQuery: string;
  isLoading: boolean;
  isSuccess: boolean;
  error: string | null;
}

interface AnalyzeActions {
  setPlaylistUrl: (url: string) => void;
  setPlaylistData: (data: PlaylistData | null) => void;
  setRangeStart: (start: string) => void;
  setRangeEnd: (end: string) => void;
  setSortBy: (sortBy: string) => void;
  setPlaybackSpeed: (speed: string) => void;
  setSearchQuery: (query: string) => void;
  setIsLoading: (loading: boolean) => void;
  setIsSuccess: (success: boolean) => void;
  setError: (error: string | null) => void;
  reset: () => void;
}

const initialState: AnalyzeState = {
  playlistUrl: '',
  playlistData: null,
  rangeStart: '1',
  rangeEnd: '100',
  sortBy: 'position',
  playbackSpeed: '1',
  searchQuery: '',
  isLoading: false,
  isSuccess: false,
  error: null
};

export const useAnalyzeStore = create<AnalyzeState & AnalyzeActions>((set) => ({
  ...initialState,
  setPlaylistUrl: (url) => set({ playlistUrl: url }),
  setPlaylistData: (data) => set({ playlistData: data }),
  setRangeStart: (start) => set({ rangeStart: start }),
  setRangeEnd: (end) => set({ rangeEnd: end }),
  setSortBy: (sortBy) => set({ sortBy: sortBy }),
  setPlaybackSpeed: (speed) => set({ playbackSpeed: speed }),
  setSearchQuery: (query) => set({ searchQuery: query }),
  setIsLoading: (loading) => set({ isLoading: loading }),
  setIsSuccess: (success) => set({ isSuccess: success }),
  setError: (error) => set({ error: error }),
  reset: () => set(initialState)
}));

Type Definitions

PlaylistData

Represents the complete playlist data structure returned from the API.

interface PlaylistData {
  playlistDetails: PlaylistDetails;  // Playlist metadata
  videos: VideoItem[];               // Array of all videos
  totalDuration: number;             // Total duration in seconds
  totalVideos: number;               // Total video count
}

Properties:

PropertyTypeDescription
playlistDetailsPlaylistDetailsPlaylist title, description, thumbnails
videosVideoItem[]Array of video objects with metadata
totalDurationnumberSum of all video durations (seconds)
totalVideosnumberTotal number of videos in playlist

AnalyzeState

The main state interface containing all analyzer state.

interface AnalyzeState {
  playlistUrl: string;
  playlistData: PlaylistData | null;
  rangeStart: string;
  rangeEnd: string;
  sortBy: string;
  playbackSpeed: string;
  searchQuery: string;
  isLoading: boolean;
  isSuccess: boolean;
  error: string | null;
}

State Properties:

PropertyTypeDefaultDescription
playlistUrlstring''YouTube playlist URL input
playlistDataPlaylistData | nullnullFetched playlist data
rangeStartstring'1'Start video index (1-based)
rangeEndstring'100'End video index (1-based)
sortBystring'position'Sort criteria: position, duration, views, likes, publish date
playbackSpeedstring'1'Playback speed multiplier (0.25 to 2)
searchQuerystring''Video title search query
isLoadingbooleanfalseData fetching in progress
isSuccessbooleanfalseFetch completed successfully
errorstring | nullnullError message if fetch failed

AnalyzeActions

Actions to update the store state.

interface AnalyzeActions {
  setPlaylistUrl: (url: string) => void;
  setPlaylistData: (data: PlaylistData | null) => void;
  setRangeStart: (start: string) => void;
  setRangeEnd: (end: string) => void;
  setSortBy: (sortBy: string) => void;
  setPlaybackSpeed: (speed: string) => void;
  setSearchQuery: (query: string) => void;
  setIsLoading: (loading: boolean) => void;
  setIsSuccess: (success: boolean) => void;
  setError: (error: string | null) => void;
  reset: () => void;
}

Store Actions

setPlaylistUrl

Updates the playlist URL input value.

setPlaylistUrl: (url: string) => void

Example:

const { setPlaylistUrl } = useAnalyzeStore();

setPlaylistUrl('https://www.youtube.com/playlist?list=PLrAXtmErZgOeiKm4sgNOknGvNjby9efdf');

setPlaylistData

Sets the fetched playlist data or clears it.

setPlaylistData: (data: PlaylistData | null) => void

Example:

const { setPlaylistData } = useAnalyzeStore();

// Set data after successful fetch
const response = await fetch('/api/playlist?id=...');
const data = await response.json();
setPlaylistData(data);

// Clear data
setPlaylistData(null);

setRangeStart

Sets the starting video index (1-based).

setRangeStart: (start: string) => void

Example:

const { setRangeStart } = useAnalyzeStore();

setRangeStart('10'); // Start from 10th video

setRangeEnd

Sets the ending video index (1-based).

setRangeEnd: (end: string) => void

Example:

const { setRangeEnd } = useAnalyzeStore();

setRangeEnd('50'); // End at 50th video

Note: setRangeEnd is automatically called with totalVideos when data is fetched.


setSortBy

Sets the sort criteria for videos.

setSortBy: (sortBy: string) => void

Valid Values:

  • 'position' - Original playlist order (default)
  • 'duration' - Sort by video duration (longest first)
  • 'views' - Sort by view count (most viewed first)
  • 'likes' - Sort by like count (most liked first)
  • 'publish date' - Sort by publish date (newest first)

Example:

const { setSortBy } = useAnalyzeStore();

setSortBy('views'); // Sort by most viewed

setPlaybackSpeed

Sets the playback speed multiplier.

setPlaybackSpeed: (speed: string) => void

Valid Values: '0.25', '0.5', '0.75', '1', '1.25', '1.5', '1.75', '2'

Example:

const { setPlaybackSpeed } = useAnalyzeStore();

setPlaybackSpeed('1.5'); // 1.5x speed

setSearchQuery

Sets the search query for filtering videos by title.

setSearchQuery: (query: string) => void

Example:

const { setSearchQuery } = useAnalyzeStore();

setSearchQuery('tutorial'); // Filter videos with "tutorial" in title

setIsLoading

Sets the loading state during data fetch.

setIsLoading: (loading: boolean) => void

Example:

const { setIsLoading } = useAnalyzeStore();

setIsLoading(true);  // Start loading
// ... fetch data
setIsLoading(false); // Stop loading

setIsSuccess

Sets the success state after successful fetch (triggers success animation).

setIsSuccess: (success: boolean) => void

Example:

const { setIsSuccess } = useAnalyzeStore();

setIsSuccess(true);  // Show success state
// Auto-reset after 2 seconds in useFetchPlaylist hook

setError

Sets the error message when fetch fails.

setError: (error: string | null) => void

Example:

const { setError } = useAnalyzeStore();

try {
  // ... fetch logic
} catch (error) {
  setError(error.message);
}

// Clear error
setError(null);

reset

Resets entire store to initial state.

reset: () => void

Example:

const { reset } = useAnalyzeStore();

reset(); // Clear all data and reset to defaults

Usage Examples

Basic Store Access

import { useAnalyzeStore } from '@/features/analyze/store/analyze-store';

function MyComponent() {
  // Access state
  const playlistUrl = useAnalyzeStore((state) => state.playlistUrl);
  const isLoading = useAnalyzeStore((state) => state.isLoading);
  
  // Access actions
  const setPlaylistUrl = useAnalyzeStore((state) => state.setPlaylistUrl);
  
  return (
    <input
      value={playlistUrl}
      onChange={(e) => setPlaylistUrl(e.target.value)}
      disabled={isLoading}
    />
  );
}

Accessing Multiple Values

import { useAnalyzeStore } from '@/features/analyze/store/analyze-store';

function PlaylistSummary() {
  // Destructure multiple values
  const { playlistData, playbackSpeed, rangeStart, rangeEnd } = useAnalyzeStore();
  
  if (!playlistData) return null;
  
  return (
    <div>
      <p>Videos: {rangeStart} to {rangeEnd}</p>
      <p>Speed: {playbackSpeed}x</p>
      <p>Total: {playlistData.totalVideos} videos</p>
    </div>
  );
}

Selective Subscription

Optimize re-renders by selecting only needed values:

import { useAnalyzeStore } from '@/features/analyze/store/analyze-store';

function SearchBar() {
  // Only re-render when searchQuery changes
  const searchQuery = useAnalyzeStore((state) => state.searchQuery);
  const setSearchQuery = useAnalyzeStore((state) => state.setSearchQuery);
  
  return (
    <input
      value={searchQuery}
      onChange={(e) => setSearchQuery(e.target.value)}
    />
  );
}

Complete Workflow Example

import { useAnalyzeStore } from '@/features/analyze/store/analyze-store';

function AnalyzeButton() {
  const {
    playlistUrl,
    setIsLoading,
    setIsSuccess,
    setError,
    setPlaylistData,
    setRangeEnd
  } = useAnalyzeStore();

  const handleAnalyze = async () => {
    // Start loading
    setIsLoading(true);
    setError(null);

    try {
      // Fetch playlist data
      const response = await fetch(`/api/playlist?id=${playlistUrl}`);
      const data = await response.json();

      if (!response.ok) {
        throw new Error(data.error);
      }

      // Update store with data
      setPlaylistData(data);
      setRangeEnd(data.totalVideos.toString());
      setIsSuccess(true);

      // Auto-reset success after 2s
      setTimeout(() => setIsSuccess(false), 2000);
    } catch (error) {
      setError(error.message);
      setTimeout(() => setError(null), 2000);
    } finally {
      setIsLoading(false);
    }
  };

  return <button onClick={handleAnalyze}>Analyze</button>;
}

State Persistence

The store does not persist state across page reloads. All state is reset when the page is refreshed.

Adding Persistence (Optional)

To add persistence, you can use Zustand's persist middleware:

import { create } from 'zustand';
import { persist } from 'zustand/middleware';

export const useAnalyzeStore = create<AnalyzeState & AnalyzeActions>()(
  persist(
    (set) => ({
      ...initialState,
      // ... actions
    }),
    {
      name: 'analyze-storage', // localStorage key
      partialize: (state) => ({
        // Only persist these fields
        playlistUrl: state.playlistUrl,
        rangeStart: state.rangeStart,
        rangeEnd: state.rangeEnd,
        sortBy: state.sortBy,
        playbackSpeed: state.playbackSpeed
      })
    }
  )
);

Store Benefits

1. Centralized State

  • Single source of truth for all analyzer state
  • Easy to debug and track state changes
  • Consistent state across components

2. Type Safety

  • Full TypeScript support
  • Autocomplete for all state and actions
  • Compile-time error checking

3. Performance

  • Fine-grained subscriptions prevent unnecessary re-renders
  • Zustand's shallow comparison optimization
  • No Provider wrapper needed

4. Developer Experience

  • Simple API (no dispatch, actions, or reducers)
  • No boilerplate
  • Easy to test
  • DevTools integration

Testing

Example tests for the store:

import { useAnalyzeStore } from './analyze-store';

describe('useAnalyzeStore', () => {
  beforeEach(() => {
    // Reset store before each test
    useAnalyzeStore.getState().reset();
  });

  test('should set playlist URL', () => {
    const { setPlaylistUrl, playlistUrl } = useAnalyzeStore.getState();
    const url = 'https://youtube.com/playlist?list=PLtest';
    
    setPlaylistUrl(url);
    
    expect(useAnalyzeStore.getState().playlistUrl).toBe(url);
  });

  test('should set loading state', () => {
    const { setIsLoading } = useAnalyzeStore.getState();
    
    setIsLoading(true);
    expect(useAnalyzeStore.getState().isLoading).toBe(true);
    
    setIsLoading(false);
    expect(useAnalyzeStore.getState().isLoading).toBe(false);
  });

  test('should reset to initial state', () => {
    const { setPlaylistUrl, setSortBy, reset } = useAnalyzeStore.getState();
    
    setPlaylistUrl('test-url');
    setSortBy('views');
    
    reset();
    
    const state = useAnalyzeStore.getState();
    expect(state.playlistUrl).toBe('');
    expect(state.sortBy).toBe('position');
  });
});