Smart Library Docs

Frontend State Management

Zustand-powered state management for loading states, global banners, and notification service integration.

Frontend State Management

The SLMS frontend uses Zustand for global state management alongside NextAuth for session/auth state and the built-in React component state for local UI concerns.


State Architecture

┌──────────────────────────────────────────┐
│              NextAuth Session            │  ← User identity, role, accessToken
├──────────────────────────────────────────┤
│           Zustand Stores                 │
│  ┌────────────────┐  ┌────────────────┐  │
│  │ useBannerStore │  │ useLoadingStore│  │
│  │ (system alerts)│  │ (request state)│  │
│  └────────────────┘  └────────────────┘  │
├──────────────────────────────────────────┤
│       Notification Service               │  ← Wraps Sonner + Banner Store
├──────────────────────────────────────────┤
│         React Component State            │  ← Local UI (form values, modals)
└──────────────────────────────────────────┘

Zustand Stores

useBannerStore

File: src/stores/useBannerStore.ts

Manages a single global alert banner (only one visible at a time).

interface BannerState {
  id: string;
  message: string;
  severity: 'info' | 'warning' | 'error' | 'critical';
  actionText?: string;
  onAction?: () => void;
  dismissible: boolean;
}

interface BannerStore {
  activeBanner: BannerState | null;
  showBanner: (banner) => void;
  hideBanner: () => void;
}

Triggers:

  • Offline detection hook (useOfflineDetection)
  • Network errors in Axios interceptor
  • Error Boundary component

useLoadingStore

File: src/stores/useLoadingStore.ts

Manages both global page-level loading and scoped, key-based request loading states.

interface LoadingStore {
  isGlobalLoading: boolean;
  setGlobalLoading: (isLoading: boolean) => void;

  activeRequests: Set<string>;
  startRequest: (key: string) => void;
  endRequest: (key: string) => void;
  isLoading: (key: string) => boolean;
}

Usage Example:

const { startRequest, endRequest, isLoading } = useLoadingStore();

const handleSave = async () => {
  startRequest('save-book');
  try {
    await api.post('/books', data);
  } finally {
    endRequest('save-book');
  }
};

// In the template:
<Button disabled={isLoading('save-book')}>
  {isLoading('save-book') ? 'Saving...' : 'Save'}
</Button>

NextAuth Session

Session state is managed entirely by NextAuth and accessed via the useSession() hook or the auth() server function:

// Client component
const { data: session } = useSession();
const role = session?.user?.role;
const accessToken = (session as any)?.accessToken;

// Server component / middleware
const session = await auth();

Session Shape

{
  user: {
    id: string;
    name: string;
    email: string;
    role: string;          // "ROLE_ADMIN" | "ROLE_LIBRARIAN" | etc.
    rollNumber: string;
    contactNumber: string;
  },
  accessToken: string;
  error?: string;           // "RefreshAccessTokenError" if refresh failed
}

Notification Service (Non-Store)

The NotificationService class (src/services/notification.service.ts) is not a Zustand store. It's a singleton class that imperatively triggers toasts and banners:

import { notification } from '@/services/notification.service';

notification.success('Saved');       // Sonner toast
notification.error('Failed');        // Sonner toast
notification.banner('Offline', 'warning');  // Updates useBannerStore
notification.clearBanner();          // Clears useBannerStore

const ok = await notification.confirm('Delete?', 'Cannot undo.');  // Promise<boolean>

This approach keeps components simple — they call notification.success() without needing to import stores or manage toast state.


Theme State

Provider: src/components/theme-provider.tsx

Uses next-themes with three modes:

  • light
  • dark
  • system (default)

The theme toggle component (theme-toggle.tsx) switches between modes. The ThemeProvider wraps the entire app in layout.tsx.

On this page