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>
This commit is contained in:
380
docs/STATE_MANAGEMENT.md
Normal file
380
docs/STATE_MANAGEMENT.md
Normal file
@@ -0,0 +1,380 @@
|
||||
# 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
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
'use client';
|
||||
|
||||
import { useExample } from '@/state';
|
||||
|
||||
export function Counter() {
|
||||
const { count, increment } = useExample();
|
||||
|
||||
return (
|
||||
<button onClick={increment}>
|
||||
Count: {count}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Using Outside React
|
||||
|
||||
```typescript
|
||||
// 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.
|
||||
|
||||
```typescript
|
||||
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.
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
// 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**
|
||||
```typescript
|
||||
// Good
|
||||
import { adminNavState } from '@/state/admin';
|
||||
import { publicNavState } from '@/state/public';
|
||||
```
|
||||
|
||||
2. **Use methods in state for complex operations**
|
||||
```typescript
|
||||
// Good
|
||||
export const state = proxy({
|
||||
count: 0,
|
||||
increment() {
|
||||
state.count += 1;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
3. **Add error handling in async methods**
|
||||
```typescript
|
||||
// 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**
|
||||
```typescript
|
||||
// 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**
|
||||
```typescript
|
||||
// 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**
|
||||
```typescript
|
||||
// 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**
|
||||
```typescript
|
||||
// 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)
|
||||
|
||||
```typescript
|
||||
// Old pattern - still works but deprecated
|
||||
import stateNav from '@/state/state-nav';
|
||||
import { authStore } from '@/store/authStore';
|
||||
```
|
||||
|
||||
### New Pattern (Recommended)
|
||||
|
||||
```typescript
|
||||
// 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.
|
||||
|
||||
```typescript
|
||||
// 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:
|
||||
|
||||
```typescript
|
||||
// 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:
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
- [Valtio Documentation](https://github.com/pmndrs/valtio)
|
||||
- [Valtio Examples](https://github.com/pmndrs/valtio/tree/main/examples)
|
||||
- [Reactivity Guide](https://docs.pmnd.rs/valtio/guides/reactivity)
|
||||
Reference in New Issue
Block a user