Files
BMAD-METHOD/demos/early-v3alpha-full-stack-app-demo/10-sharded-docs/front-end-state-management.md

5.1 KiB

State Management In-Depth

This document is a granulated shard from the main "5-front-end-architecture.md" focusing on "State Management In-Depth".

This section expands on the State Management strategy chosen (Zustand) and outlined in the "Overall Frontend Philosophy & Patterns".

  • Chosen Solution: Zustand (Latest version, as per architecture.txt)
  • Rationale: Zustand was chosen for its simplicity, small bundle size, and unopinionated nature, suitable for BMad DiCaster's relatively simple frontend state needs (e.g., podcast player status). Server-side data is primarily managed by Next.js Server Components.

Store Structure / Slices

Global client-side state will be organized into distinct "slices" within store/slices/. Components can import and use individual stores directly.

  • Conventions:

    • Each slice in its own file: store/slices/camelCaseSlice.ts.
    • Define state interface, initial state, and action functions.
  • Core Slice: podcastPlayerSlice.ts (for MVP)

    • Purpose: Manages the state of the podcast player (current track, playback status, time, volume).

    • Source File: store/slices/podcastPlayerSlice.ts

    • State Shape (Example):

      interface PodcastTrack {
        id: string; // Could be newsletterId or a specific audio ID
        title: string;
        audioUrl: string;
        duration?: number; // in seconds
      }
      
      interface PodcastPlayerState {
        currentTrack: PodcastTrack | null;
        isPlaying: boolean;
        currentTime: number; // in seconds
        volume: number; // 0 to 1
        isLoading: boolean;
        error: string | null;
      }
      
      interface PodcastPlayerActions {
        loadTrack: (track: PodcastTrack) => void;
        play: () => void;
        pause: () => void;
        setCurrentTime: (time: number) => void;
        setVolume: (volume: number) => void;
        setError: (message: string | null) => void;
        resetPlayer: () => void;
      }
      
    • Key Actions: loadTrack, play, pause, setCurrentTime, setVolume, setError, resetPlayer.

    • Zustand Store Definition:

      import { create } from "zustand";
      
      // Previously defined interfaces: PodcastTrack, PodcastPlayerState, PodcastPlayerActions
      
      const initialPodcastPlayerState: PodcastPlayerState = {
        currentTrack: null,
        isPlaying: false,
        currentTime: 0,
        volume: 0.75,
        isLoading: false,
        error: null,
      };
      
      export const usePodcastPlayerStore = create<
        PodcastPlayerState & PodcastPlayerActions
      >((set) => ({
        ...initialPodcastPlayerState,
        loadTrack: (track) =>
          set({
            currentTrack: track,
            isLoading: true, // Assume loading until actual audio element confirms
            error: null,
            isPlaying: false, // Usually don't autoplay on load
            currentTime: 0,
          }),
        play: () =>
          set((state) => {
            if (!state.currentTrack) return {}; // No track loaded
            return { isPlaying: true, isLoading: false, error: null };
          }),
        pause: () => set({ isPlaying: false }),
        setCurrentTime: (time) => set({ currentTime: time }),
        setVolume: (volume) => set({ volume: Math.max(0, Math.min(1, volume)) }),
        setError: (message) =>
          set({ error: message, isLoading: false, isPlaying: false }),
        resetPlayer: () => set({ ...initialPodcastPlayerState }),
      }));
      

Key Selectors

Selectors are functions that derive data from the store state. With Zustand, state is typically accessed directly from the hook, but memoized selectors can be created with libraries like reselect if complex derived data is needed, though for simple cases direct access is fine.

  • Convention: For direct state access, components will use: const { currentTrack, isPlaying, play } = usePodcastPlayerStore();
  • Example Selectors (if using reselect or similar, for more complex derivations later):
    • selectCurrentTrackTitle: Returns state.currentTrack?.title || 'No track loaded'.
    • selectIsPodcastPlaying: Returns state.isPlaying.

Key Actions / Reducers / Thunks

Zustand actions are functions defined within the create call that use set to update state. Asynchronous operations (like fetching data, though less common for Zustand which is often for UI state) can be handled by calling async functions within these actions and then calling set upon completion.

  • Convention: Actions are part of the store hook: const { loadTrack } = usePodcastPlayerStore();.
  • Asynchronous Example (Conceptual, if a slice needed to fetch data):
    // In a hypothetical userSettingsSlice.ts
    // fetchUserSettings: async () => {
    //   set({ isLoading: true });
    //   try {
    //     const settings = await api.fetchUserSettings(); // api is an imported service
    //     set({ userSettings: settings, isLoading: false });
    //   } catch (error) {
    //     set({ error: 'Failed to fetch settings', isLoading: false });
    //   }
    // }
    
    For BMad DiCaster MVP, most data fetching is via Server Components. Client-side async actions in Zustand would primarily be for client-specific operations not directly tied to server data fetching.