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.