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
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:
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
// 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
// Global file drop handling
// Used by components/globalDropZone.tsx4. LoadingContext
Global loading state management.
Location: src/contexts/loading-context.tsx
// Manages global loading indicators
// Prevents multiple overlapping loaders5. AudioPlayerTableContext
Manages track table and player synchronization.
Location: src/contexts/audioplayer-table-context.tsx
// Syncs track selection between table and player6. NavigatorContext
Navigation state and history.
Location: src/contexts/navigatorContext.tsx
// Manages navigation breadcrumbs and history7. RecentTracksContext
Recently accessed tracks for quick access.
Location: src/contexts/recentTracksContext.tsx
// Tracks user's recent track viewsJotai Store
For atomic state that needs to persist or be accessed globally.
Checkout Store
Location: src/store/checkout.ts
import { atom } from 'jotai';
// Checkout state atoms
export const checkoutAtom = atom({
plan: null,
interval: 'monthly',
// ...
});Quick Cart Store
Location: src/store/quick-cart/
// Shopping cart state for subscription upgradesNextAuth Session State
Authentication state managed by NextAuth.
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
// 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:
// 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:
'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:
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:
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:
// src/hooks/use-layout.ts
export function useLayout() {
const [layout, setLayout] = useLocalStorage(
'layout',
LAYOUT_OPTIONS.BERYLLIUM
);
return { layout, setLayout };
}useUpdateSession
Refreshes session data:
// 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
// Enable in development
import { DevTools } from 'jotai-devtools';
function App() {
return (
<>
<DevTools />
{/* ... */}
</>
);
}Best Practices
- Use Context for feature-level state (audio player, notifications)
- Use Jotai for atomic shared state (cart, preferences)
- Use local state for component-specific UI (modals, dropdowns)
- Avoid prop drilling - use contexts for deeply nested state
- Memoize context values to prevent unnecessary re-renders:
const value = useMemo(() => ({
tracks,
setTracks,
// ...
}), [tracks, /* dependencies */]);
return <Context.Provider value={value}>{children}</Context.Provider>;