Files
lcbp3/specs/05-decisions/ADR-014-state-management.md

8.9 KiB

ADR-014: State Management Strategy

Status: Accepted Date: 2025-12-01 Decision Makers: Frontend Team Related Documents: Frontend Guidelines, ADR-011: App Router


Context and Problem Statement

ระบบ LCBP3-DMS ต้องการจัดการ Global State เช่น User session, Notifications, UI preferences ต้องเลือก State Management solution ที่เหมาะสม

ปัญหาที่ต้องแก้:

  1. Global State: จัดการ State ที่ใช้ร่วมกันทั้งแอปอย่างไร
  2. Server State: จัดการข้อมูลจาก API อย่างไร
  3. Performance: หลีกเลี่ยง Unnecessary re-renders
  4. Type Safety: Type-safe state management
  5. Bundle Size: ไม่ทำให้ Bundle ใหญ่เกินไป

Decision Drivers

  • Performance: Minimal re-renders
  • 📦 Bundle Size: เล็กที่สุด
  • 🎯 Simplicity: เรียนรู้และใช้งานง่าย
  • Type Safety: TypeScript support
  • 🔄 Server State: จัดการ API data ได้ดี

Considered Options

Option 1: Redux Toolkit

Pros:

  • Industry standard
  • DevTools ดี
  • Middleware support

Cons:

  • Boilerplate มาก
  • Bundle size ใหญ่ (~40kb)
  • Complexity สูง
  • Overkill สำหรับ App ส่วนใหญ่

Option 2: React Context API

Pros:

  • Built-in (no dependencies)
  • Simple

Cons:

  • Performance issues (re-render ทั้ง tree)
  • ไม่เหมาะสำหรับ Complex state
  • ต้องจัดการ Optimization เอง

Option 3: Zustand

Props:

  • Lightweight: ~1.2kb only
  • Simple API: เรียนรู้ง่าย
  • Performance: Selective re-renders
  • TypeScript: Full support
  • No boilerplate
  • DevTools support

Cons:

  • Smaller community กว่า Redux

Option 4: React Query (TanStack Query) for Server State

Pros:

  • Specialized: จัดการ Server state ได้ดีที่สุด
  • Caching: Auto cache management
  • Refetching: Auto refetch on focus
  • TypeScript: Excellent support

Cons:

  • เฉพาะ Server state (ต้องใช้คู่กับ Client state solution)

Decision Outcome

Chosen Option: Zustand (Client State) + Native Fetch with Server Components (Server State)

Rationale

For Client State (UI state, Preferences):

  • Use Zustand - lightweight และเรียนรู้ง่าย

For Server State (API data):

  • Use Server Components + SWR (เฉพาะที่จำเป็น)
  • Server Components ดึงข้อมูลฝั่ง Server ไม่ต้องจัดการ state

Implementation Details

1. Install Zustand

npm install zustand

2. Create Global Store (User Session)

// File: lib/stores/auth-store.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface User {
  user_id: number;
  username: string;
  email: string;
  first_name: string;
  last_name: string;
}

interface AuthState {
  user: User | null;
  token: string | null;
  isAuthenticated: boolean;

  // Actions
  setAuth: (user: User, token: string) => void;
  logout: () => void;
}

export const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      user: null,
      token: null,
      isAuthenticated: false,

      setAuth: (user, token) =>
        set({
          user,
          token,
          isAuthenticated: true,
        }),

      logout: () =>
        set({
          user: null,
          token: null,
          isAuthenticated: false,
        }),
    }),
    {
      name: 'auth-storage', // LocalStorage key
    }
  )
);

3. Use Store in Components

// File: components/header.tsx
'use client';

import { useAuthStore } from '@/lib/stores/auth-store';
import { Button } from '@/components/ui/button';

export function Header() {
  const { user, logout } = useAuthStore();

  return (
    <header className="flex justify-between items-center p-4">
      <div>Welcome, {user?.first_name}</div>
      <Button onClick={logout}>Logout</Button>
    </header>
  );
}

4. Notifications Store

// File: lib/stores/notification-store.ts
import { create } from 'zustand';

interface Notification {
  id: string;
  type: 'success' | 'error' | 'info';
  message: string;
}

interface NotificationState {
  notifications: Notification[];

  addNotification: (notification: Omit<Notification, 'id'>) => void;
  removeNotification: (id: string) => void;
  clearAll: () => void;
}

export const useNotificationStore = create<NotificationState>((set) => ({
  notifications: [],

  addNotification: (notification) =>
    set((state) => ({
      notifications: [
        ...state.notifications,
        { ...notification, id: Math.random().toString() },
      ],
    })),

  removeNotification: (id) =>
    set((state) => ({
      notifications: state.notifications.filter((n) => n.id !== id),
    })),

  clearAll: () => set({ notifications: [] }),
}));

5. Server State with Server Components

// File: app/(dashboard)/correspondences/page.tsx
// Server Component - No state management needed!

import { getCorrespondences } from '@/lib/api/correspondences';

export default async function CorrespondencesPage() {
  // Fetch directly on server
  const correspondences = await getCorrespondences();

  return (
    <div>
      <h1>Correspondences</h1>
      {correspondences.map((item) => (
        <div key={item.id}>{item.subject}</div>
      ))}
    </div>
  );
}

6. Client-Side Fetching (with SWR for real-time data)

npm install swr
// File: components/correspondences/realtime-list.tsx
'use client';

import useSWR from 'swr';

const fetcher = (url: string) => fetch(url).then((r) => r.json());

export function RealtimeCorrespondenceList() {
  const { data, error, isLoading, mutate } = useSWR(
    '/api/correspondences',
    fetcher,
    {
      refreshInterval: 30000, // Auto refresh every 30s
    }
  );

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error loading data</div>;

  return (
    <div>
      {data.map((item) => (
        <div key={item.id}>{item.subject}</div>
      ))}
      <button onClick={() => mutate()}>Refresh</button>
    </div>
  );
}

7. UI Preferences Store

// File: lib/stores/ui-store.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface UIState {
  sidebarCollapsed: boolean;
  theme: 'light' | 'dark';

  toggleSidebar: () => void;
  setTheme: (theme: 'light' | 'dark') => void;
}

export const useUIStore = create<UIState>()(
  persist(
    (set) => ({
      sidebarCollapsed: false,
      theme: 'light',

      toggleSidebar: () =>
        set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })),

      setTheme: (theme) => set({ theme }),
    }),
    {
      name: 'ui-preferences',
    }
  )
);

State Management Patterns

When to Use Zustand (Client State)

Use Zustand for:

  • User authentication state
  • UI preferences (theme, sidebar state)
  • Notifications/Toasts
  • Shopping cart (if applicable)
  • Form wizard state
  • Modal state (global)

When to Use Server Components (Server State)

Use Server Components for:

  • Initial data loading
  • Static/semi-static data
  • SEO-important content
  • Data that doesn't need real-time updates

When to Use SWR (Client-Side Server State)

Use SWR for:

  • Real-time data (notifications count)
  • Polling/Auto-refresh data
  • User-specific data that changes often
  • Optimistic UI updates

Consequences

Positive Consequences

  1. Lightweight: Zustand ~1.2kb
  2. Simple: Easy to learn and use
  3. Performance: Selective re-renders
  4. No Boilerplate: Clean API
  5. Type Safe: Full TypeScript support
  6. Persistent: Easy LocalStorage persist

Negative Consequences

  1. Smaller Ecosystem: กว่า Redux
  2. Less Tooling: DevTools ไม่ครบเท่า Redux

Mitigation Strategies

  • Documentation: Document common patterns
  • Code Examples: Provide store templates
  • Testing: Unit test stores thoroughly


References


Last Updated: 2025-12-01 Next Review: 2026-06-01