- 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>
8.0 KiB
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
-
Separate admin and public state
// Good import { adminNavState } from '@/state/admin'; import { publicNavState } from '@/state/public'; -
Use methods in state for complex operations
// Good export const state = proxy({ count: 0, increment() { state.count += 1; }, }); -
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; } } -
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
-
Don't mutate state directly in render
// Bad function Component() { state.count += 1; // Don't do this in render return <div>{state.count}</div>; } -
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 -
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)
// 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>;
}