State Management
State management patterns, optimistic updates, and conflict resolution in Smart Shelf.
State Management
This section covers state management patterns, optimistic updates, and conflict resolution strategies used in Smart Shelf.
State Management Overview
Smart Shelf implements a hybrid state management approach combining React's built-in state management with custom hooks for complex data flows and optimistic updates.
State Architecture
graph TD
A[User Action] --> B[Optimistic Update]
B --> C[Local State]
B --> D[API Call]
D --> E{Success?}
E -->|Yes| F[Confirm Update]
E -->|No| G[Rollback State]
G --> H[Show Error]
F --> I[Sync with Server]
style A fill:#e1f5fe
style C fill:#fff3e0
style F fill:#e8f5e8
style G fill:#ffebee
Optimistic Updates
Optimistic Mutation Hook
// hooks/use-optimistic-mutation.ts
import { useState, useCallback } from 'react';
import { toast } from 'sonner';
interface OptimisticMutationOptions<T, R> {
mutationFn: (data: T) => Promise<R>;
onSuccess?: (result: R, variables: T) => void;
onError?: (error: Error, variables: T) => void;
onSettled?: () => void;
}
export function useOptimisticMutation<T, R>({
mutationFn,
onSuccess,
onError,
onSettled,
}: OptimisticMutationOptions<T, R>) {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const mutate = useCallback(
async (
variables: T,
optimisticUpdate?: () => void,
rollback?: () => void
) => {
setIsLoading(true);
setError(null);
// Apply optimistic update immediately
if (optimisticUpdate) {
optimisticUpdate();
}
try {
const result = await mutationFn(variables);
// Success - keep optimistic update
onSuccess?.(result, variables);
return result;
} catch (err) {
const error = err instanceof Error ? err : new Error('Mutation failed');
// Failure - rollback optimistic update
if (rollback) {
rollback();
}
setError(error);
onError?.(error, variables);
// Show error toast
toast.error(error.message);
throw error;
} finally {
setIsLoading(false);
onSettled?.();
}
},
[mutationFn, onSuccess, onError, onSettled]
);
return {
mutate,
isLoading,
error,
};
}
// Usage example
export function useUpdateProduct() {
const { mutate } = useOptimisticMutation({
mutationFn: async (data: { id: string; updates: Partial<Product> }) => {
const response = await fetch(`/api/products/${data.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data.updates),
});
if (!response.ok) {
throw new Error('Failed to update product');
}
return response.json();
},
onSuccess: () => {
toast.success('Product updated successfully');
},
});
return {
updateProduct: (id: string, updates: Partial<Product>, optimisticUpdate: () => void, rollback: () => void) =>
mutate({ id, updates }, optimisticUpdate, rollback),
};
}
Conflict Resolution
Data Sync Manager
// lib/sync/conflict-resolution.ts
export interface ConflictResolution<T> {
strategy: 'client-wins' | 'server-wins' | 'merge' | 'user-choice';
resolver?: (client: T, server: T) => T;
}
export class DataSyncManager<T extends { id: string; updated_at: string }> {
private conflictQueue = new Map<string, { client: T; server: T }>();
async resolveConflict(
clientData: T,
serverData: T,
resolution: ConflictResolution<T>
): Promise<T> {
switch (resolution.strategy) {
case 'client-wins':
return clientData;
case 'server-wins':
return serverData;
case 'merge':
if (resolution.resolver) {
return resolution.resolver(clientData, serverData);
}
// Default merge strategy
return {
...serverData,
...clientData,
updated_at: new Date().toISOString(),
};
case 'user-choice':
// Queue for user resolution
this.conflictQueue.set(clientData.id, {
client: clientData,
server: serverData,
});
throw new ConflictError('User resolution required', clientData.id);
default:
return serverData;
}
}
getConflicts(): Array<{ id: string; client: T; server: T }> {
return Array.from(this.conflictQueue.entries()).map(([id, data]) => ({
id,
...data,
}));
}
resolveUserConflict(id: string, resolution: T): void {
this.conflictQueue.delete(id);
// Apply user's resolution
this.applyResolution(id, resolution);
}
private async applyResolution(id: string, data: T): Promise<void> {
// Sync resolved data back to server
await fetch(`/api/sync/resolve/${id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
}
}
export class ConflictError extends Error {
constructor(message: string, public recordId: string) {
super(message);
this.name = 'ConflictError';
}
}
Conflict Resolution Component
// components/sync/conflict-resolver.tsx
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
interface ConflictResolverProps<T> {
conflicts: Array<{ id: string; client: T; server: T }>;
onResolve: (id: string, resolution: T) => void;
onResolveAll: (strategy: 'client-wins' | 'server-wins') => void;
}
export function ConflictResolver<T extends Record<string, any>>({
conflicts,
onResolve,
onResolveAll,
}: ConflictResolverProps<T>) {
const [selectedResolutions, setSelectedResolutions] = useState<Record<string, T>>({});
if (conflicts.length === 0) {
return null;
}
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">
Data Conflicts Detected ({conflicts.length})
</h3>
<div className="space-x-2">
<Button
variant="outline"
onClick={() => onResolveAll('client-wins')}
>
Keep All Local Changes
</Button>
<Button
variant="outline"
onClick={() => onResolveAll('server-wins')}
>
Keep All Server Changes
</Button>
</div>
</div>
{conflicts.map((conflict) => (
<Card key={conflict.id}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
Data Conflict
<Badge variant="destructive">Requires Resolution</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
<div>
<h4 className="font-medium mb-2">Your Changes</h4>
<div className="space-y-1 text-sm">
{Object.entries(conflict.client).map(([key, value]) => (
<div key={key}>
<span className="font-medium">{key}:</span> {String(value)}
</div>
))}
</div>
<Button
className="mt-2"
variant="outline"
onClick={() => onResolve(conflict.id, conflict.client)}
>
Keep My Changes
</Button>
</div>
<div>
<h4 className="font-medium mb-2">Server Changes</h4>
<div className="space-y-1 text-sm">
{Object.entries(conflict.server).map(([key, value]) => (
<div key={key}>
<span className="font-medium">{key}:</span> {String(value)}
</div>
))}
</div>
<Button
className="mt-2"
variant="outline"
onClick={() => onResolve(conflict.id, conflict.server)}
>
Keep Server Changes
</Button>
</div>
</div>
</CardContent>
</Card>
))}
</div>
);
}
State Management Patterns
Global State Context
// context/app-state-context.tsx
import { createContext, useContext, useReducer, ReactNode } from 'react';
interface AppState {
user: User | null;
warehouse: Warehouse | null;
notifications: Notification[];
isOnline: boolean;
syncStatus: 'synced' | 'syncing' | 'conflict' | 'error';
}
type AppAction =
| { type: 'SET_USER'; payload: User | null }
| { type: 'SET_WAREHOUSE'; payload: Warehouse | null }
| { type: 'ADD_NOTIFICATION'; payload: Notification }
| { type: 'REMOVE_NOTIFICATION'; payload: string }
| { type: 'SET_ONLINE_STATUS'; payload: boolean }
| { type: 'SET_SYNC_STATUS'; payload: AppState['syncStatus'] };
const initialState: AppState = {
user: null,
warehouse: null,
notifications: [],
isOnline: true,
syncStatus: 'synced',
};
function appReducer(state: AppState, action: AppAction): AppState {
switch (action.type) {
case 'SET_USER':
return { ...state, user: action.payload };
case 'SET_WAREHOUSE':
return { ...state, warehouse: action.payload };
case 'ADD_NOTIFICATION':
return {
...state,
notifications: [...state.notifications, action.payload]
};
case 'REMOVE_NOTIFICATION':
return {
...state,
notifications: state.notifications.filter(n => n.id !== action.payload),
};
case 'SET_ONLINE_STATUS':
return { ...state, isOnline: action.payload };
case 'SET_SYNC_STATUS':
return { ...state, syncStatus: action.payload };
default:
return state;
}
}
const AppStateContext = createContext<{
state: AppState;
dispatch: React.Dispatch<AppAction>;
} | null>(null);
export function AppStateProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(appReducer, initialState);
return (
<AppStateContext.Provider value={{ state, dispatch }}>
{children}
</AppStateContext.Provider>
);
}
export function useAppState() {
const context = useContext(AppStateContext);
if (!context) {
throw new Error('useAppState must be used within AppStateProvider');
}
return context;
}
Local Storage Persistence
// hooks/use-persisted-state.ts
import { useState, useEffect } from 'react';
export function usePersistedState<T>(
key: string,
defaultValue: T
): [T, React.Dispatch<React.SetStateAction<T>>] {
const [state, setState] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch (error) {
console.warn(`Error reading localStorage key "${key}":`, error);
return defaultValue;
}
});
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(state));
} catch (error) {
console.warn(`Error setting localStorage key "${key}":`, error);
}
}, [key, state]);
return [state, setState];
}
// Usage
export function useUserPreferences() {
return usePersistedState('userPreferences', {
theme: 'light',
language: 'en',
warehouse: null,
});
}
State Management Best Practices
Implementation Guidelines
- Optimistic Updates: Apply changes immediately for better UX
- Conflict Resolution: Handle data conflicts gracefully
- Error Recovery: Provide rollback mechanisms for failed operations
- State Normalization: Keep state flat and normalized when possible
- Selective Updates: Only update affected components
Performance Tips
- Use React's built-in optimization features (useMemo, useCallback)
- Implement proper dependency arrays in useEffect
- Consider state co-location for component-specific state
- Use context sparingly to avoid unnecessary re-renders
- Implement proper cleanup in useEffect hooks
This state management system ensures Smart Shelf maintains consistent state while providing excellent user experience through optimistic updates and robust conflict resolution.