Files
metabuilder/docs/reference/STATE_MANAGEMENT_GUIDE.md
johndoe6345789 b3e17e7dd4 feat: Add troubleshooting guide and enhance act scripts
- Created a new troubleshooting guide in README.md for common issues and testing problems.
- Updated package.json to include new act commands for linting, type checking, building, and diagnosing workflows.
- Added a pre-commit hook script to validate workflows before commits.
- Enhanced run-act.sh script with logging, Docker checks, and improved output formatting.
- Improved test-workflows.sh with an interactive menu and performance tracking.
- Introduced setup-act.sh for quick setup and testing of act integration.
2025-12-25 13:16:45 +00:00

944 lines
24 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🎭 State Management Consolidation Guide
**Document Date:** December 25, 2025
**Purpose:** Unified strategy for scattered state across components, context, and database
**Target:** Consistent, testable, debuggable state patterns
---
## 🎯 Current Situation
### State Scatter Map
```
Component State (useState) [LOCAL]
├─ UI state (expanded, focused)
├─ Form inputs
├─ Modal open/closed
└─ Temporary data
Context API [GLOBAL]
├─ Auth context
├─ Theme context
├─ User settings
└─ Notifications
Database State (Prisma) [PERSISTENT]
├─ User data
├─ Configuration
├─ Preferences
└─ Audit logs
Third-party Libraries [EXTERNAL]
├─ React Query (caching)
├─ React Hook Form (forms)
├─ Redux (if used)
└─ Zustand (if used)
```
**Problems:**
1. ❌ No clear decision criteria → inconsistent choices
2. ❌ Overlapping patterns → duplicate state
3. ❌ No caching strategy → unnecessary database calls
4. ❌ Context sprawl → 10+ providers
5. ❌ No clear data flow → hard to debug
---
## ✅ Unified State Architecture
### 4 State Categories
```
┌─────────────────────────────────────────────────────────┐
│ STATE MANAGEMENT HIERARCHY │
├─────────────────────────────────────────────────────────┤
│ │
│ CATEGORY 1: LOCAL STATE (useState) │
│ Duration: Session │
│ Scope: Single component │
│ Persistence: None │
│ Max items: 2-3 per component │
│ ✓ UI toggles, temporary form data │
│ │
├─────────────────────────────────────────────────────────┤
│ │
│ CATEGORY 2: GLOBAL STATE (Context) │
│ Duration: Session │
│ Scope: Entire app │
│ Persistence: Session storage │
│ Max providers: 3-4 │
│ ✓ Auth, theme, notifications │
│ │
├─────────────────────────────────────────────────────────┤
│ │
│ CATEGORY 3: DATABASE STATE (Prisma + React) │
│ Duration: Permanent │
│ Scope: Multi-session │
│ Persistence: Database │
│ ✓ User data, configs, preferences │
│ │
├─────────────────────────────────────────────────────────┤
│ │
│ CATEGORY 4: CACHE STATE (React Query) │
│ Duration: Temporary with invalidation │
│ Scope: Single or multiple components │
│ Persistence: Memory │
│ ✓ API responses, expensive queries │
│ │
└─────────────────────────────────────────────────────────┘
```
---
## 📋 Decision Tree
```
START: I need to store some data
Is this data user-specific or tenant-specific?
├─ NO → Is it needed globally across the app?
│ ├─ NO → LOCAL STATE (useState)
│ │ └─ Keep it simple, short-lived
│ └─ YES → Is it frequently accessed?
│ ├─ YES → GLOBAL STATE (Context)
│ │ └─ Auth, theme, settings
│ └─ NO → CACHE STATE (React Query)
│ └─ Fetch once, reuse
└─ YES → Will it outlive the current session?
├─ NO → LOCAL STATE or CACHE
│ └─ Temporary user interactions
└─ YES → DATABASE STATE (Prisma)
└─ Save to DB, load on app start
```
---
## 1⃣ LOCAL STATE: useState
### Definition
Ephemeral state that lives only during current component render cycle.
### When to Use
```typescript
// ✅ DO: Use for UI state
const [isExpanded, setIsExpanded] = useState(false);
const [activeTab, setActiveTab] = useState(0);
const [hoverIndex, setHoverIndex] = useState(-1);
// ✅ DO: Use for form inputs before submission
const [firstName, setFirstName] = useState('');
const [email, setEmail] = useState('');
// ✅ DO: Use for temporary data
const [searchResults, setSearchResults] = useState([]);
// ❌ DON'T: Use for data that should persist
const [userPreferences, setUserPreferences] = useState({});
// → Move to DATABASE STATE
// ❌ DON'T: Use for app-wide settings
const [isDarkMode, setIsDarkMode] = useState(false);
// → Move to GLOBAL STATE or DATABASE STATE
// ❌ DON'T: Duplicate server state locally
const [userData, setUserData] = useState(null);
// → Use CACHE STATE with React Query
```
### Patterns
**Simple Toggle:**
```typescript
export function AccordionItem({ title, children }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>
{isOpen ? '▼' : '▶'} {title}
</button>
{isOpen && <div>{children}</div>}
</div>
);
}
```
**Form Inputs:**
```typescript
export function UserForm({ onSubmit }) {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
return (
<form onSubmit={(e) => {
e.preventDefault();
onSubmit({ name, email });
setName(''); // Clear after submit
setEmail('');
}}>
<input value={name} onChange={(e) => setName(e.target.value)} />
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<button type="submit">Submit</button>
</form>
);
}
```
**Multiple Related States (Use Object):**
```typescript
export function Modal() {
const [state, setState] = useState({
isOpen: false,
position: { x: 0, y: 0 },
size: { width: 500, height: 300 },
});
const setIsOpen = (isOpen) =>
setState((s) => ({ ...s, isOpen }));
const setPosition = (position) =>
setState((s) => ({ ...s, position }));
// ... use state.isOpen, setIsOpen, setPosition
}
```
### Limits
- **Max per component:** 2-3 items
- **Max data size:** < 1 KB
- **Duration:** Current session only
- **Scope:** Single component only
---
## 2⃣ GLOBAL STATE: Context API
### Definition
Application-wide state shared across many components without prop drilling.
### When to Use
```typescript
// ✅ DO: Authentication context
const AuthContext = createContext();
// → User data, tokens, login/logout functions
// ✅ DO: Theme context
const ThemeContext = createContext();
// → Dark/light mode, color scheme
// ✅ DO: Notifications context
const NotificationsContext = createContext();
// → Toast/alert messages, dismissal
// ❌ DON'T: Component-specific state
// → Keep in component with useState
// ❌ DON'T: Frequently changing data
// → Use CACHE STATE instead, Context updates = expensive re-renders
// ❌ DON'T: Server state
// → Use CACHE STATE with React Query
```
### Maximum 3-4 Global Contexts
```
┌─ AuthContext
│ ├─ user
│ ├─ isAuthenticated
│ └─ login/logout
├─ ThemeContext
│ ├─ isDarkMode
│ └─ setDarkMode
├─ NotificationsContext
│ ├─ notifications[]
│ ├─ addNotification
│ └─ removeNotification
└─ (Optional) SettingsContext
├─ userSettings
└─ updateSettings
```
**Never add:**
- ❌ Global component visibility state
- ❌ Global form state
- ❌ Global selection state
- ❌ Global search state
---
### Pattern: AuthContext
```typescript
// src/context/AuthContext.tsx
import { createContext, useContext, useState, useEffect } from 'react';
interface AuthContextType {
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
// Restore user from session storage on mount
useEffect(() => {
const stored = sessionStorage.getItem('user');
if (stored) {
setUser(JSON.parse(stored));
}
setIsLoading(false);
}, []);
const login = async (email: string, password: string) => {
setIsLoading(true);
try {
const response = await api.post('/auth/login', { email, password });
setUser(response.user);
sessionStorage.setItem('user', JSON.stringify(response.user));
} finally {
setIsLoading(false);
}
};
const logout = () => {
setUser(null);
sessionStorage.removeItem('user');
};
return (
<AuthContext.Provider
value={{
user,
isAuthenticated: !!user,
isLoading,
login,
logout,
}}
>
{children}
</AuthContext.Provider>
);
}
// Custom hook for using auth
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
```
**Usage in components:**
```typescript
export function Header() {
const { user, isAuthenticated, logout } = useAuth();
if (!isAuthenticated) {
return <div>Not logged in</div>;
}
return (
<header>
<span>Welcome, {user?.name}</span>
<button onClick={logout}>Logout</button>
</header>
);
}
```
---
## 3⃣ DATABASE STATE: Prisma + React State
### Definition
Persistent data stored in database, loaded into React state, cached during session.
### When to Use
```typescript
// ✅ DO: User preferences
const [userPrefs, setUserPrefs] = useState<UserPreferences | null>(null);
useEffect(() => {
Database.getUserPreferences(userId).then(setUserPrefs);
}, [userId]);
// ✅ DO: Configuration data
const [config, setConfig] = useState<Config | null>(null);
useEffect(() => {
Database.getConfig(tenantId).then(setConfig);
}, [tenantId]);
// ✅ DO: Lists of items from database
const [items, setItems] = useState<Item[]>([]);
useEffect(() => {
Database.getItems(tenantId).then(setItems);
}, [tenantId]);
// ❌ DON'T: Cache without React Query
// → Too complex to manage updates/refetch
// ❌ DON'T: Forget tenantId in queries
const items = await Database.getItems(); // ❌ WRONG
const items = await Database.getItems({ tenantId }); // ✅ RIGHT
```
### Pattern: Database State Hook
```typescript
// src/hooks/useUserPreferences.ts
export interface UserPreferences {
theme: 'light' | 'dark';
language: string;
notifications: boolean;
}
export function useUserPreferences(userId: string) {
const [prefs, setPrefs] = useState<UserPreferences | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
// Load preferences on mount
useEffect(() => {
const load = async () => {
try {
setLoading(true);
const data = await Database.getUserPreferences(userId);
setPrefs(data);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
};
load();
}, [userId]);
// Update preferences (write-through cache)
const updatePrefs = useCallback(
async (updates: Partial<UserPreferences>) => {
try {
const updated = await Database.updateUserPreferences(userId, updates);
setPrefs(updated);
} catch (err) {
setError(err as Error);
// Revert optimistic update if it fails
const current = await Database.getUserPreferences(userId);
setPrefs(current);
}
},
[userId]
);
return { prefs, loading, error, updatePrefs };
}
```
**Usage:**
```typescript
export function SettingsPanel() {
const { user } = useAuth();
const { prefs, loading, updatePrefs } = useUserPreferences(user.id);
if (loading) return <div>Loading...</div>;
return (
<div>
<label>
Dark Mode:
<input
type="checkbox"
checked={prefs?.theme === 'dark'}
onChange={(e) =>
updatePrefs({ theme: e.target.checked ? 'dark' : 'light' })
}
/>
</label>
</div>
);
}
```
### Database Schema Example
```prisma
// prisma/schema.prisma
model UserPreferences {
id String @id @default(cuid())
userId String @unique
user User @relation(fields: [userId], references: [id])
tenantId String
// Preferences data
theme String @default("light") // 'light' | 'dark'
language String @default("en")
notifications Boolean @default(true)
sidebarWidth Int @default(250)
// Metadata
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([userId, tenantId])
}
```
---
## 4⃣ CACHE STATE: React Query
### Definition
Temporary cached state for API/database queries with automatic invalidation.
### When to Use
```typescript
// ✅ DO: API responses
const { data: workflows } = useQuery({
queryKey: ['workflows', userId],
queryFn: () => api.get(`/workflows?user=${userId}`),
});
// ✅ DO: Expensive database queries
const { data: analytics } = useQuery({
queryKey: ['analytics', dateRange],
queryFn: () => Database.getAnalytics(dateRange),
});
// ✅ DO: Frequently changing data
const { data: notifications } = useQuery({
queryKey: ['notifications'],
queryFn: () => api.get('/notifications'),
refetchInterval: 5000, // Poll every 5 seconds
});
// ❌ DON'T: Use for always-fresh data
// → Use state + useEffect instead
// ❌ DON'T: Forget queryKey
const { data } = useQuery({
queryFn: () => fetch('/api/data'),
// ❌ No queryKey means no caching
});
```
### Pattern: React Query Hook
```typescript
// src/hooks/useWorkflows.ts
import { useQuery } from '@tanstack/react-query';
export function useWorkflows(userId: string) {
return useQuery({
queryKey: ['workflows', userId],
queryFn: async () => {
const response = await Database.getWorkflows({ userId });
return response;
},
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 10, // 10 minutes cache
});
}
export function useWorkflowDetails(workflowId: string) {
return useQuery({
queryKey: ['workflow', workflowId],
queryFn: async () => {
return Database.getWorkflow(workflowId);
},
enabled: !!workflowId, // Don't fetch if no ID
});
}
export function useUpdateWorkflow() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: UpdateWorkflowData) =>
Database.updateWorkflow(data),
onSuccess: (newData) => {
// Invalidate related queries
queryClient.invalidateQueries({
queryKey: ['workflows'],
});
// Update specific query
queryClient.setQueryData(['workflow', newData.id], newData);
},
});
}
```
**Usage:**
```typescript
export function WorkflowsList() {
const { data: workflows, isLoading, error } = useWorkflows('user-123');
const updateMutation = useUpdateWorkflow();
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
{workflows?.map((workflow) => (
<div key={workflow.id}>
<h3>{workflow.name}</h3>
<button
onClick={() =>
updateMutation.mutate({
id: workflow.id,
status: 'paused',
})
}
disabled={updateMutation.isPending}
>
Pause
</button>
</div>
))}
</div>
);
}
```
### Setup QueryClient
```typescript
// src/main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 1 minute
gcTime: 1000 * 60 * 10, // 10 minutes
retry: 1,
retryDelay: (attemptIndex) =>
Math.min(1000 * 2 ** attemptIndex, 30000),
},
},
});
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
</QueryClientProvider>
);
}
```
---
## 🔄 State Flow Diagram
```
┌─ USER ACTION (click, input, etc)
├─ LOCAL STATE (useState) ──────────→ Update immediately
│ └─ Re-render component
├─ GLOBAL STATE (Context) ──────────→ Update provider
│ └─ Re-render all consumers
├─ DATABASE STATE (Prisma + useState)
│ ├─ Read: Load on mount
│ │ └─ useState → Re-render
│ └─ Write: Save to DB
│ ├─ Optimistic update to state
│ ├─ Database operation
│ └─ Invalidate cache if fails
└─ CACHE STATE (React Query)
├─ Read: useQuery
│ ├─ Return cached if fresh
│ └─ Fetch if stale
├─ Write: useMutation
│ ├─ Optimistic update
│ ├─ Server operation
│ ├─ Success: invalidate queries
│ └─ Error: rollback
└─ Auto-invalidate: staleTime expiry
```
---
## 📊 State Pattern Decision Matrix
| Scenario | Local | Context | Database | Cache |
|----------|-------|---------|----------|-------|
| UI toggle | ✅ | ❌ | ❌ | ❌ |
| Form input | ✅ | ❌ | ❌ | ❌ |
| Current user | ❌ | ✅ | ✅ | ❌ |
| App settings | ❌ | ✅ | ✅ | ❌ |
| Theme | ❌ | ✅ | ✅ | ❌ |
| User preferences | ❌ | ❌ | ✅ | ❌ |
| API response | ❌ | ❌ | ❌ | ✅ |
| Search results | ✅ | ❌ | ❌ | ✅ |
| Analytics data | ❌ | ❌ | ❌ | ✅ |
| List of items | ❌ | ❌ | ✅ | ✅ |
| Notification | ✅ | ✅ | ❌ | ❌ |
---
## ⚠️ Anti-Patterns to Avoid
### ❌ Anti-Pattern 1: Global Context for Everything
```typescript
// ❌ WRONG: Monolithic context with everything
const GlobalContext = createContext();
// Contains: user, theme, notifications, modals, forms, ...
// Result: Re-renders entire app on any change ❌
```
**Fix:** Split into separate concerns
```typescript
// ✅ RIGHT: Separate contexts
const AuthContext = createContext(); // User info
const ThemeContext = createContext(); // Theme settings
const NotificationsContext = createContext(); // Alerts
```
---
### ❌ Anti-Pattern 2: Database Reads Scattered
```typescript
// ❌ WRONG: Each component fetches directly
function ComponentA() {
const [data, setData] = useState(null);
useEffect(() => {
Database.getData().then(setData);
}, []);
}
function ComponentB() {
const [data, setData] = useState(null);
useEffect(() => {
Database.getData().then(setData); // Same query, duplicate fetch!
}, []);
}
```
**Fix:** Use custom hook or React Query
```typescript
// ✅ RIGHT: Centralized query
export function useData() {
return useQuery({
queryKey: ['data'],
queryFn: () => Database.getData(),
});
}
function ComponentA() {
const { data } = useData(); // Uses cache
}
function ComponentB() {
const { data } = useData(); // Same cached result
}
```
---
### ❌ Anti-Pattern 3: Forgetting tenantId
```typescript
// ❌ WRONG: No tenant isolation
const items = await Database.getItems();
// This might return items from OTHER tenants!
```
**Fix:** Always include tenantId
```typescript
// ✅ RIGHT: Tenant-aware query
const items = await Database.getItems({
tenantId: currentUser.tenantId,
});
```
---
### ❌ Anti-Pattern 4: useState for Server State
```typescript
// ❌ WRONG: Duplicate server state locally
const [user, setUser] = useState(null);
useEffect(() => {
Database.getUser(userId).then(setUser);
}, [userId]);
// Problem: Manual cache invalidation needed ❌
```
**Fix:** Use React Query
```typescript
// ✅ RIGHT: Let React Query manage cache
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => Database.getUser(userId),
});
// Automatic cache management ✅
```
---
## 🎯 Migration Checklist
### Step 1: Audit (1 week)
- [ ] List all useState instances
- [ ] List all useContext instances
- [ ] Map all Database reads
- [ ] Identify cache opportunities
### Step 2: Define (1 week)
- [ ] Choose context structure (max 3-4)
- [ ] Define database models
- [ ] Set up React Query
- [ ] Document patterns
### Step 3: Implement (2-3 weeks)
- [ ] Create standard hooks
- [ ] Migrate component state
- [ ] Migrate context usage
- [ ] Add React Query
### Step 4: Test (1 week)
- [ ] Unit tests for hooks
- [ ] Integration tests
- [ ] E2E tests
- [ ] Performance testing
### Step 5: Deploy (Ongoing)
- [ ] Gradual rollout
- [ ] Monitor performance
- [ ] Fix issues
- [ ] Document learnings
---
## 📚 Templates
### Custom Hook Template
```typescript
// src/hooks/use[Name].ts
import { useCallback, useEffect, useState } from 'react';
export interface [Name]State {
// Define your state shape
data: any;
loading: boolean;
error: Error | null;
}
export function use[Name](/* params */) {
const [state, setState] = useState<[Name]State>({
data: null,
loading: false,
error: null,
});
// Load data on mount
useEffect(() => {
// Load logic
}, [/* dependencies */]);
// Action handlers
const action = useCallback(async (/* params */) => {
// Action logic
}, []);
return { ...state, action };
}
```
### Context Provider Template
```typescript
// src/context/[Name]Context.tsx
import { createContext, useContext, useState } from 'react';
interface [Name]ContextType {
// Define your context
}
const [Name]Context = createContext<[Name]ContextType | undefined>(undefined);
export function [Name]Provider({ children }) {
const [state, setState] = useState({
// Initial state
});
return (
<[Name]Context.Provider value={{ state, setState }}>
{children}
</[Name]Context.Provider>
);
}
export function use[Name]() {
const context = useContext([Name]Context);
if (!context) {
throw new Error('use[Name] must be used within [Name]Provider');
}
return context;
}
```
---
## 🚀 Implementation Order
1. **Setup React Query** (4 hours)
- Install packages
- Create QueryClient
- Set up provider
- Add dev tools
2. **Create Context Structure** (6 hours)
- AuthContext
- ThemeContext
- NotificationsContext
- Custom hooks
3. **Migrate Database State** (20 hours)
- Identify all database reads
- Create hooks for each query
- Replace scattered useState
- Add React Query where applicable
4. **Consolidate Component State** (15 hours)
- Audit all useState usage
- Move global state to context
- Move persistent state to database
- Keep only UI state local
5. **Testing** (10 hours)
- Unit tests for hooks
- Integration tests
- E2E tests
---
## ✅ Success Metrics
- [ ] Max 3-4 global context providers
- [ ] 80% reduction in useState instances in containers
- [ ] 100% of database reads use hooks
- [ ] 90%+ cache hit rate for repeated queries
- [ ] Zero duplicate database fetches
- [ ] All multi-tenant queries include tenantId
- [ ] <100ms time to interactive
- [ ] Zero console warnings/errors
---
**Generated:** December 25, 2025
**Next Review:** After Phase 2 completion