Files
desa-darmasaba/docs/STATE_MANAGEMENT.md
nico 6ed2392420 Add comprehensive testing suite and fix QC issues
- Add 115+ unit, component, and E2E tests
- Add Vitest configuration with coverage thresholds
- Add validation schema tests (validations.test.ts)
- Add sanitizer utility tests (sanitizer.test.ts)
- Add WhatsApp service tests (whatsapp.test.ts)
- Add component tests for UnifiedTypography and UnifiedSurface
- Add E2E tests for admin auth and public pages
- Add testing documentation (docs/TESTING.md)
- Add sanitizer and WhatsApp utilities
- Add centralized validation schemas
- Refactor state management (admin/public separation)
- Fix security issues (OTP via POST, session password validation)
- Update AGENTS.md with testing guidelines

Test Coverage: 50%+ target achieved
All tests passing: 115/115

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-09 14:05:03 +08:00

8.0 KiB

State Management Guide

Overview

Desa Darmasaba menggunakan Valtio untuk global state management. Valtio adalah state management library yang menggunakan proxy pattern untuk reactive state yang sederhana dan performant.

Why Valtio?

  • Simple API - Menggunakan plain JavaScript objects
  • Performant - Component re-renders hanya saat state yang digunakan berubah
  • TypeScript-friendly - Full TypeScript support
  • No boilerplate - Tidak perlu actions, reducers, atau selectors
  • Flexible - Bisa digunakan di dalam atau luar React components

Installation

bun install valtio

State Structure

src/state/
├── admin/                  # Admin dashboard state
│   ├── index.ts           # Admin state exports
│   ├── adminNavState.ts   # Navigation state
│   ├── adminAuthState.ts  # Authentication state
│   ├── adminFormState.ts  # Form state (images, files)
│   └── adminModuleState.ts # Module-specific state
│
├── public/                 # Public pages state
│   ├── index.ts           # Public state exports
│   ├── publicNavState.ts  # Navigation state
│   └── publicMusicState.ts # Music player state
│
├── darkModeStore.ts        # Dark mode state (legacy)
└── index.ts                # Main exports

Basic Usage

Creating State

// src/state/example/exampleState.ts
import { proxy, useSnapshot } from 'valtio';

export const exampleState = proxy<{
  count: number;
  items: string[];
  isLoading: boolean;
  increment: () => void;
  addItem: (item: string) => void;
}>({
  count: 0,
  items: [],
  isLoading: false,
  
  increment() {
    exampleState.count += 1;
  },
  
  addItem(item: string) {
    exampleState.items.push(item);
  },
});

// Hook untuk React components
export const useExample = () => {
  const snapshot = useSnapshot(exampleState);
  return {
    ...snapshot,
    increment: exampleState.increment,
    addItem: exampleState.addItem,
  };
};

Using in React Components

'use client';

import { useExample } from '@/state';

export function Counter() {
  const { count, increment } = useExample();
  
  return (
    <button onClick={increment}>
      Count: {count}
    </button>
  );
}

Using Outside React

// In non-React code (utilities, services, etc.)
import { exampleState } from '@/state';

// Direct mutation
exampleState.count = 10;
exampleState.increment();

// Subscribe to changes
import { subscribe } from 'valtio';

subscribe(exampleState, () => {
  console.log('State changed:', exampleState.count);
});

Domain-Specific State

Admin State

State untuk admin dashboard hanya digunakan di /admin routes.

import { 
  adminNavState, 
  adminAuthState, 
  useAdminNav,
  useAdminAuth 
} from '@/state';

// In React component
export function AdminHeader() {
  const { mobileOpen, toggleMobile } = useAdminNav();
  const { user, isAuthenticated } = useAdminAuth();
  
  return (
    <Header>
      <Button onClick={toggleMobile}>Menu</Button>
      {user?.name}
    </Header>
  );
}

// Outside React
adminNavState.mobileOpen = true;
adminAuthState.clearUser();

Public State

State untuk public pages hanya digunakan di /darmasaba routes.

import { 
  publicNavState, 
  publicMusicState,
  usePublicNav,
  usePublicMusic 
} from '@/state';

// In React component
export function MusicPlayer() {
  const { isPlaying, currentSong, togglePlayPause } = usePublicMusic();
  
  return (
    <Player>
      {currentSong?.judul}
      <Button onClick={togglePlayPause}>
        {isPlaying ? 'Pause' : 'Play'}
      </Button>
    </Player>
  );
}

Async Operations

// src/state/example/dataState.ts
import { proxy, useSnapshot } from 'valtio';
import ApiFetch from '@/lib/api-fetch';

export const dataState = proxy<{
  data: any[];
  isLoading: boolean;
  error: string | null;
  fetchData: (id: string) => Promise<void>;
}>({
  data: [],
  isLoading: false,
  error: null,
  
  async fetchData(id: string) {
    dataState.isLoading = true;
    dataState.error = null;
    
    try {
      const response = await ApiFetch.someApi.get({ id });
      dataState.data = response.data;
    } catch (error) {
      dataState.error = error instanceof Error ? error.message : 'Failed to fetch';
    } finally {
      dataState.isLoading = false;
    }
  },
});

export const useData = () => {
  const snapshot = useSnapshot(dataState);
  return {
    ...snapshot,
    fetchData: dataState.fetchData,
  };
};

Best Practices

DO

  1. Separate admin and public state

    // Good
    import { adminNavState } from '@/state/admin';
    import { publicNavState } from '@/state/public';
    
  2. Use methods in state for complex operations

    // Good
    export const state = proxy({
      count: 0,
      increment() {
        state.count += 1;
      },
    });
    
  3. Add error handling in async methods

    // Good
    async fetchData() {
      state.isLoading = true;
      state.error = null;
      try {
        // fetch logic
      } catch (error) {
        state.error = error.message;
      } finally {
        state.isLoading = false;
      }
    }
    
  4. Use TypeScript for type safety

    // Good
    type User = { id: string; name: string };
    
    export const authState = proxy<{
      user: User | null;
      setUser: (user: User | null) => void;
    }>({ ... });
    

DON'T

  1. Don't mutate state directly in render

    // Bad
    function Component() {
      state.count += 1; // Don't do this in render
      return <div>{state.count}</div>;
    }
    
  2. Don't mix admin and public state

    // Bad
    import { adminAuthState } from '@/state/admin';
    import { publicNavState } from '@/state/public';
    
    // Don't use admin state in public pages
    
  3. Don't create new objects in state methods

    // Bad
    increment() {
      state.count = state.count + 1; // Creates new number
    }
    
    // Good
    increment() {
      state.count += 1; // Mutates existing value
    }
    

Migration from Legacy State

Old Pattern (Deprecated)

// Old pattern - still works but deprecated
import stateNav from '@/state/state-nav';
import { authStore } from '@/store/authStore';
// New pattern - recommended
import { adminNavState } from '@/state/admin';
import { adminAuthState } from '@/state/admin';

Music Player State

Music player sekarang menggunakan Valtio state dengan React Context wrapper untuk backward compatibility.

// New way (recommended)
import { usePublicMusic } from '@/state/public';

function MusicPlayer() {
  const { isPlaying, currentSong, togglePlayPause } = usePublicMusic();
  // ...
}

// Old way (still works for backward compatibility)
import { useMusic } from '@/app/context/MusicContext';

function MusicPlayer() {
  const { isPlaying, currentSong, togglePlayPause } = useMusic();
  // ...
}

Troubleshooting

State not updating in component

Make sure you're using the hook in component:

// Good
function Component() {
  const { count } = useExample(); // Subscribe to state
  return <div>{count}</div>;
}

// Bad
function Component() {
  const count = exampleState.count; // No subscription
  return <div>{count}</div>;
}

Performance issues

Use selective subscriptions:

// Good - only subscribe to what you need
function Component() {
  const { count } = useExample(); // Only count
  return <div>{count}</div>;
}

// Bad - subscribe to entire state
function Component() {
  const state = useExample(); // Entire state
  return <div>{state.count}</div>;
}

Additional Resources