Skip to content

04 - State Management

Overview

Wavic WebApp uses a hybrid state management approach combining:

  • React Context for feature-specific state (audio player, dropzone)
  • Jotai for atomic state (checkout, cart)
  • NextAuth for authentication state
  • Component State for local UI state

Context Architecture

┌─────────────────────────────────────────────────────────────┐
│                     ROOT LAYOUT                             │
├─────────────────────────────────────────────────────────────┤
│  AuthProvider (NextAuth)                                    │
│    └── NotificationContextProvider                          │
│          └── ThemeProvider (next-themes)                    │
│                └── MainComponent                            │
│                      └── Page Content                       │
└─────────────────────────────────────────────────────────────┘

Core Contexts

1. WaveContext (Audio Player State)

The most critical context, managing the audio player, waveform, and track playback.

Location: src/contexts/waveContext.tsx

typescript
type WaveContextProps = {
  // Track Management
  tracks: TTrack[];
  setTracks: React.Dispatch<React.SetStateAction<TTrack[]>>;
  track: TTrack | null;
  setTrack: React.Dispatch<React.SetStateAction<TTrack | null>>;
  tracksWithAudio: TTrack[];
  setTracksWithAudio: React.Dispatch<React.SetStateAction<TTrack[]>>;

  // Audio Selection
  selectedAudioTrack: TFile | null;
  setSelectedAudioTrack: React.Dispatch<React.SetStateAction<TFile | null>>;

  // Version Management
  versions: IVersion[];
  setVersions: React.Dispatch<React.SetStateAction<IVersion[]>>;
  versionNum: IVersion;
  setVersionNum: React.Dispatch<React.SetStateAction<IVersion>>;

  // Waveform State
  wavesurfer: React.MutableRefObject<WaveSurfer | null>;
  waveformState: IWaveFormState; // { currentTime, totalTime }
  setWaveformState: React.Dispatch<React.SetStateAction<IWaveFormState>>;

  // Playback Controls
  playMusic: boolean;
  setPlayMusic: React.Dispatch<React.SetStateAction<boolean>>;
  shuffle: boolean;
  setShuffle: React.Dispatch<React.SetStateAction<boolean>>;
  loop: boolean;
  setLoop: React.Dispatch<React.SetStateAction<boolean>>;

  // Volume
  volumeState: VolumeState; // { volume, lastVolume }
  setVolumeState: React.Dispatch<React.SetStateAction<VolumeState>>;

  // Comments
  carousel: IComment[];
  setCarousel: React.Dispatch<React.SetStateAction<IComment[]>>;
  handleComments: (comments: IComment[]) => void;

  // Actions
  fetchTrackData: (trackId: string) => Promise<void>;
  handleOnSeek: (time: number) => void;

  // Loading
  isLoading: boolean;
  error: Error | null;
};

Usage:

typescript
import { useWaveContext } from '@/contexts/waveContext';

function TrackPlayer() {
  const {
    track,
    playMusic,
    setPlayMusic,
    waveformState
  } = useWaveContext();

  return (
    <div>
      <span>{track?.name}</span>
      <span>{waveformState.currentTime} / {waveformState.totalTime}</span>
      <button onClick={() => setPlayMusic(!playMusic)}>
        {playMusic ? 'Pause' : 'Play'}
      </button>
    </div>
  );
}

2. NotificationContext

Manages real-time notifications.

Location: src/contexts/notificationContext.tsx

typescript
// Context provides notification state and actions
function NotificationContextProvider({ children }) {
  const [notifications, setNotifications] = useState([]);

  // WebSocket or polling for new notifications
  // ...

  return (
    <NotificationContext.Provider value={{ notifications, ... }}>
      {children}
    </NotificationContext.Provider>
  );
}

3. DropZoneContext

Manages file upload state and drag-and-drop.

Location: src/contexts/drop-zone-context.tsx

typescript
// Global file drop handling
// Used by components/globalDropZone.tsx

4. LoadingContext

Global loading state management.

Location: src/contexts/loading-context.tsx

typescript
// Manages global loading indicators
// Prevents multiple overlapping loaders

5. AudioPlayerTableContext

Manages track table and player synchronization.

Location: src/contexts/audioplayer-table-context.tsx

typescript
// Syncs track selection between table and player

6. NavigatorContext

Navigation state and history.

Location: src/contexts/navigatorContext.tsx

typescript
// Manages navigation breadcrumbs and history

7. RecentTracksContext

Recently accessed tracks for quick access.

Location: src/contexts/recentTracksContext.tsx

typescript
// Tracks user's recent track views

Jotai Store

For atomic state that needs to persist or be accessed globally.

Checkout Store

Location: src/store/checkout.ts

typescript
import { atom } from 'jotai';

// Checkout state atoms
export const checkoutAtom = atom({
  plan: null,
  interval: 'monthly',
  // ...
});

Quick Cart Store

Location: src/store/quick-cart/

typescript
// Shopping cart state for subscription upgrades

NextAuth Session State

Authentication state managed by NextAuth.

typescript
import { useSession } from 'next-auth/react';

function MyComponent() {
  const { data: session, status } = useSession();

  // session.user - User object
  // session.token - JWT token
  // status - 'loading' | 'authenticated' | 'unauthenticated'

  if (status === 'loading') return <Loading />;
  if (status === 'unauthenticated') return <SignIn />;

  return <Dashboard user={session.user} />;
}

Session Structure

typescript
// types/next-auth.d.ts
declare module 'next-auth' {
  interface Session {
    user: IUser;
    token: string;
    iat: number;
    exp: number;
    jti: string;
  }
}

State Patterns

1. Provider Composition

Root layout composes all providers:

typescript
// src/app/layout.tsx
export default async function RootLayout({ children }) {
  const session = await getServerSession(authOptions);

  return (
    <html lang="en">
      <body>
        <AuthProvider session={session}>
          <NotificationContextProvider>
            <ThemeProvider>
              <MainComponent>{children}</MainComponent>
            </ThemeProvider>
          </NotificationContextProvider>
        </AuthProvider>
      </body>
    </html>
  );
}

2. Context + Server Actions

Components fetch data via Server Actions and store in Context:

typescript
'use client';

export default function ArtistPage({ artistId }) {
  const { data: session } = useSession();
  const [artist, setArtist] = useState(null);

  useEffect(() => {
    async function load() {
      // Server Action call
      const data = await getArtist(session.token, artistId);
      setArtist(data);
    }
    load();
  }, [artistId]);

  return <ArtistView artist={artist} />;
}

3. Optimistic Updates

For better UX, update state before API confirmation:

typescript
function deleteTrack(trackId) {
  // Optimistically remove from state
  setTracks(tracks.filter((t) => t.id !== trackId));

  // Then call API
  try {
    await deleteTrackAction(token, trackId);
  } catch (error) {
    // Revert on error
    setTracks(originalTracks);
    toast.error('Failed to delete');
  }
}

4. Local Storage Persistence

Using the use-local-storage hook:

typescript
import { useLocalStorage } from '@/hooks/use-local-storage';

function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');

  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      Toggle Theme
    </button>
  );
}

Custom Hooks for State

useLayout

Manages layout selection:

typescript
// src/hooks/use-layout.ts
export function useLayout() {
  const [layout, setLayout] = useLocalStorage(
    'layout',
    LAYOUT_OPTIONS.BERYLLIUM
  );
  return { layout, setLayout };
}

useUpdateSession

Refreshes session data:

typescript
// src/hooks/use-update-session.ts
export function useUpdateSession() {
  const { update } = useSession();

  const updateSession = async (data) => {
    await update(data);
  };

  return { updateSession };
}

State Debugging

React DevTools

  • View component state and context values
  • Track re-renders and state changes

Jotai DevTools

typescript
// Enable in development
import { DevTools } from 'jotai-devtools';

function App() {
  return (
    <>
      <DevTools />
      {/* ... */}
    </>
  );
}

Best Practices

  1. Use Context for feature-level state (audio player, notifications)
  2. Use Jotai for atomic shared state (cart, preferences)
  3. Use local state for component-specific UI (modals, dropdowns)
  4. Avoid prop drilling - use contexts for deeply nested state
  5. Memoize context values to prevent unnecessary re-renders:
typescript
const value = useMemo(() => ({
  tracks,
  setTracks,
  // ...
}), [tracks, /* dependencies */]);

return <Context.Provider value={value}>{children}</Context.Provider>;

Ctrl-Audio Platform Documentation