State Management Guide
Comprehensive guide to managing state in Constellation.
State Management Strategy
Choose the right state management approach based on scope:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ State Management Hierarchy โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ 1. Local Component State (useState) โ
โ โ Single component, no sharing needed โ
โ โ
โ 2. Custom Hooks (useHook) โ
โ โ Multiple components, reusable logic โ
โ โ
โ 3. Context API + Hooks โ
โ โ Global state, medium scope โ
โ โ
โ 4. Server-side Caching (React Cache) โ
โ โ Data fetching, expensive operations โ
โ โ
โ 5. External Solutions (React Query, Redux) โ
โ โ Complex state, synchronization needs โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
1. Local Component State
Best for: Form inputs, UI toggles, temporary data
'use client';
import { useState } from 'react';
export function FormComponent() {
const [formData, setFormData] = useState({
name: '',
email: '',
});
const handleChange = (field: string, value: string) => {
setFormData(prev => ({
...prev,
[field]: value,
}));
};
return (
<form>
<input
value={formData.name}
onChange={(e) => handleChange('name', e.target.value)}
/>
</form>
);
}
When to Use
- โ Form inputs
- โ Modal open/close
- โ Loading states
- โ Temporary UI state
- โ Single component only
2. Custom Hooks
Best for: Reusable stateful logic
// src/hooks/use-form.ts
import { useState, useCallback } from 'react';
interface FormState {
[key: string]: string | number | boolean;
}
export function useForm<T extends FormState>(initialValues: T) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState<Record<string, string>>({});
const [touched, setTouched] = useState<Record<string, boolean>>({});
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setValues(prev => ({ ...prev, [name]: value }));
}, []);
const handleBlur = useCallback((e: React.FocusEvent<HTMLInputElement>) => {
const { name } = e.target;
setTouched(prev => ({ ...prev, [name]: true }));
}, []);
const resetForm = useCallback(() => {
setValues(initialValues);
setErrors({});
setTouched({});
}, [initialValues]);
return {
values,
errors,
touched,
handleChange,
handleBlur,
resetForm,
setFieldValue: (field: string, value: any) => {
setValues(prev => ({ ...prev, [field]: value }));
},
};
}
Usage:
export function LoginForm() {
const form = useForm({ email: '', password: '' });
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Submit form.values
};
return (
<form onSubmit={handleSubmit}>
<input
name="email"
value={form.values.email}
onChange={form.handleChange}
/>
</form>
);
}
When to Use
- โ Reusable logic
- โ Used in multiple components
- โ Complex state with side effects
- โ Encapsulated logic
3. Context API
Best for: Global app state
Creating Context
// src/components/context/AuthContext.tsx
'use client';
import { createContext, useContext, useState, ReactNode } from 'react';
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}
interface AuthContextType {
user: User | null;
isLoading: boolean;
isAuthenticated: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
updateUser: (user: User) => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
const login = async (email: string, password: string) => {
setIsLoading(true);
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
const userData = await response.json();
setUser(userData);
} finally {
setIsLoading(false);
}
};
const logout = async () => {
setUser(null);
await fetch('/api/auth/logout', { method: 'POST' });
};
const updateUser = (newUser: User) => {
setUser(newUser);
};
return (
<AuthContext.Provider
value={{
user,
isLoading,
isAuthenticated: !!user,
login,
logout,
updateUser,
}}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
Using Context
// src/components/UserProfile.tsx
'use client';
import { useAuth } from '@/src/components/context/AuthContext';
export function UserProfile() {
const { user, logout } = useAuth();
if (!user) return null;
return (
<div>
<p>Welcome, {user.name}</p>
<button onClick={logout}>Logout</button>
</div>
);
}
Provider Setup
// src/app/layout.tsx
import { AuthProvider } from '@/src/components/context/AuthContext';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html>
<body>
<AuthProvider>
{children}
</AuthProvider>
</body>
</html>
);
}
When to Use
- โ Global app state (auth, theme)
- โ Data needed by many components
- โ Configuration values
- โ User preferences
4. Server-side Caching
Best for: Expensive data fetching
// src/lib/cache.ts
import { cache } from 'react';
import { apiClient } from '@/src/lib/api';
// Cache per request cycle
export const getUserData = cache(async (userId: string) => {
console.log('Fetching user:', userId);
return apiClient.get(`/users/${userId}`);
});
// Cache with revalidation
export const getWorkspaces = cache(async () => {
return apiClient.get('/workspaces');
});
Usage:
// src/app/dashboard/page.tsx
import { getUserData } from '@/src/lib/cache';
export default async function Dashboard() {
const user = await getUserData('current');
return <div>Welcome, {user.name}</div>;
}
When to Use
- โ Expensive database queries
- โ API calls in Server Components
- โ Data needed across multiple renders
- โ Avoid N+1 queries
5. External State Management
For advanced needs, consider:
React Query
import { useQuery } from '@tanstack/react-query';
export function UserProfile() {
const { data: user, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => apiClient.get(`/users/${userId}`),
staleTime: 5 * 60 * 1000, // 5 minutes
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>User: {user.name}</div>;
}
When to Use
- โ Complex data fetching
- โ Need caching & synchronization
- โ Background updates needed
- โ Mutations with side effects
Pattern Comparison
| Pattern | Scope | Complexity | Use Case |
|---|---|---|---|
| useState | Local | Simple | Form inputs, UI state |
| Custom Hook | Multiple components | Medium | Reusable logic |
| Context API | Global | Medium | App state, theme |
| React Query | Data fetching | High | API caching, sync |
| Redux | Very large | High | Enterprise apps |
Best Practices
โ Do's
- โ Keep state as local as possible
- โ Lift state only when needed
- โ Use custom hooks for logic
- โ Memoize context to prevent unnecessary renders
- โ Use context for infrequently changing data
- โ Combine multiple contexts if needed
- โ Type your state with TypeScript
โ Don'ts
- โ Don't put all state in global context
- โ Don't update context too frequently
- โ Don't use context for rapidly changing data
- โ Don't forget to memoize context providers
- โ Don't use deeply nested contexts
- โ Don't update unrelated state together
- โ Don't ignore TypeScript warnings
Performance Optimization
Memoized Context Provider
// Prevent unnecessary re-renders
export function OptimizedAuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const value = useMemo(() => ({
user,
setUser,
}), [user]);
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
Split Contexts
// Instead of one large context with everything
<AuthContext.Provider>
<ThemeContext.Provider>
<UserPreferencesContext.Provider>
{children}
</UserPreferencesContext.Provider>
</ThemeContext.Provider>
</AuthContext.Provider>
Selective Subscription
// Only subscribe to the parts you need
const { user } = useAuth(); // Just user
const { login } = useAuth(); // Just login
const { user, login } = useAuth(); // Multiple
Testing State
Testing Hooks
import { renderHook, act } from '@testing-library/react';
import { useForm } from '@/src/hooks/use-form';
it('should handle form changes', () => {
const { result } = renderHook(() => useForm({ name: '' }));
act(() => {
result.current.setFieldValue('name', 'John');
});
expect(result.current.values.name).toBe('John');
});
Testing Context
import { render, screen } from '@testing-library/react';
import { AuthProvider } from '@/src/components/context/AuthContext';
it('should provide user data', () => {
render(
<AuthProvider>
<UserProfile />
</AuthProvider>
);
expect(screen.getByText(/Welcome/)).toBeInTheDocument();
});
Migration Guide
From Redux to Context API
If migrating from Redux:
- Create contexts for major stores
- Move reducers to custom hooks
- Test thoroughly
- Update components gradually
- Remove Redux dependencies
Debugging State
React DevTools
- Install React DevTools extension
- Open DevTools
- Go to Components tab
- Inspect component state
- View context values
State Logging
export function useDebugState<T>(state: T, label: string) {
useEffect(() => {
console.log(`${label}:`, state);
}, [state, label]);
}
// Usage
useDebugState(user, 'User');
Related Documentation
- ๐๏ธ ARCHITECTURE.md - State management architecture
- ๐ฃ HOOKS.md - Custom hooks reference
- ๐ COMPONENTS.md - Component patterns
Last Updated: January 2026