# State Management for Drag-and-Drop ## Table of Contents - [React State Patterns](#react-state-patterns) - [Optimistic Updates](#optimistic-updates) - [Undo/Redo Functionality](#undoredo-functionality) - [Persistence Strategies](#persistence-strategies) - [Multi-User Synchronization](#multi-user-synchronization) - [Error Recovery](#error-recovery) ## React State Patterns ### Basic Drag State Management ```tsx interface DragState { isDragging: boolean; draggedItem: any | null; draggedFrom: string | null; draggedOver: string | null; initialPosition: { x: number; y: number } | null; currentPosition: { x: number; y: number } | null; } function useDragState() { const [dragState, setDragState] = useState({ isDragging: false, draggedItem: null, draggedFrom: null, draggedOver: null, initialPosition: null, currentPosition: null, }); const startDrag = useCallback((item: any, from: string, position: { x: number; y: number }) => { setDragState({ isDragging: true, draggedItem: item, draggedFrom: from, draggedOver: null, initialPosition: position, currentPosition: position, }); }, []); const updateDrag = useCallback((over: string | null, position: { x: number; y: number }) => { setDragState(prev => ({ ...prev, draggedOver: over, currentPosition: position, })); }, []); const endDrag = useCallback(() => { setDragState({ isDragging: false, draggedItem: null, draggedFrom: null, draggedOver: null, initialPosition: null, currentPosition: null, }); }, []); return { dragState, startDrag, updateDrag, endDrag, }; } ``` ### Context-Based State Management ```tsx // DragDropContext.tsx interface DragDropContextValue { items: Record; moveItem: (item: Item, fromContainer: string, toContainer: string, toIndex: number) => void; reorderItems: (container: string, fromIndex: number, toIndex: number) => void; isDragging: boolean; draggedItem: Item | null; canDrop: (container: string) => boolean; } const DragDropContext = createContext(null); export function DragDropProvider({ children, initialItems }) { const [items, setItems] = useState(initialItems); const [draggedItem, setDraggedItem] = useState(null); const moveItem = useCallback(( item: Item, fromContainer: string, toContainer: string, toIndex: number ) => { setItems(prev => { const newItems = { ...prev }; // Remove from source newItems[fromContainer] = newItems[fromContainer].filter(i => i.id !== item.id); // Add to destination newItems[toContainer] = [ ...newItems[toContainer].slice(0, toIndex), item, ...newItems[toContainer].slice(toIndex), ]; return newItems; }); }, []); const reorderItems = useCallback(( container: string, fromIndex: number, toIndex: number ) => { setItems(prev => { const newItems = { ...prev }; const containerItems = [...newItems[container]]; const [movedItem] = containerItems.splice(fromIndex, 1); containerItems.splice(toIndex, 0, movedItem); newItems[container] = containerItems; return newItems; }); }, []); const canDrop = useCallback((container: string) => { // Custom drop rules const maxItems = 10; return items[container]?.length < maxItems; }, [items]); const value = useMemo( () => ({ items, moveItem, reorderItems, isDragging: draggedItem !== null, draggedItem, canDrop, }), [items, moveItem, reorderItems, draggedItem, canDrop] ); return ( setDraggedItem(event.active.data.current?.item)} onDragEnd={() => setDraggedItem(null)} > {children} ); } export function useDragDrop() { const context = useContext(DragDropContext); if (!context) { throw new Error('useDragDrop must be used within DragDropProvider'); } return context; } ``` ### Reducer Pattern for Complex State ```tsx type DragAction = | { type: 'START_DRAG'; payload: { item: Item; source: string } } | { type: 'UPDATE_DRAG'; payload: { over: string; position: Position } } | { type: 'END_DRAG'; payload: { target: string; index: number } } | { type: 'CANCEL_DRAG' } | { type: 'REORDER'; payload: { container: string; from: number; to: number } } | { type: 'MOVE'; payload: { item: Item; from: string; to: string; index: number } }; interface DragDropState { containers: Record; dragState: { isDragging: boolean; item: Item | null; source: string | null; target: string | null; }; history: DragDropState[]; historyIndex: number; } function dragDropReducer(state: DragDropState, action: DragAction): DragDropState { switch (action.type) { case 'START_DRAG': return { ...state, dragState: { isDragging: true, item: action.payload.item, source: action.payload.source, target: null, }, }; case 'UPDATE_DRAG': return { ...state, dragState: { ...state.dragState, target: action.payload.over, }, }; case 'END_DRAG': { if (!state.dragState.item || !state.dragState.source) { return state; } const newContainers = { ...state.containers }; const { item, source } = state.dragState; const { target, index } = action.payload; // Remove from source newContainers[source] = newContainers[source].filter(i => i.id !== item.id); // Add to target newContainers[target] = [ ...newContainers[target].slice(0, index), item, ...newContainers[target].slice(index), ]; // Add to history for undo const newState = { containers: newContainers, dragState: { isDragging: false, item: null, source: null, target: null, }, history: [...state.history.slice(0, state.historyIndex + 1), state], historyIndex: state.historyIndex + 1, }; return newState; } case 'CANCEL_DRAG': return { ...state, dragState: { isDragging: false, item: null, source: null, target: null, }, }; default: return state; } } ``` ## Optimistic Updates ### Optimistic UI Pattern ```tsx interface OptimisticUpdate { id: string; action: () => Promise; optimistic: T; rollback: T; timestamp: number; } function useOptimisticUpdates() { const [pending, setPending] = useState>>(new Map()); const [failed, setFailed] = useState>(new Map()); const executeOptimistic = async ( id: string, action: () => Promise, optimistic: T, rollback: T ) => { // Apply optimistic update immediately setPending(prev => new Map(prev).set(id, { id, action, optimistic, rollback, timestamp: Date.now(), })); try { // Execute the actual action const result = await action(); // Remove from pending on success setPending(prev => { const next = new Map(prev); next.delete(id); return next; }); return result; } catch (error) { // Mark as failed and rollback setFailed(prev => new Map(prev).set(id, error as Error)); // Apply rollback setPending(prev => { const next = new Map(prev); const update = next.get(id); if (update) { next.set(id, { ...update, optimistic: rollback }); } return next; }); throw error; } }; const retry = async (id: string) => { const update = pending.get(id); if (!update) return; setFailed(prev => { const next = new Map(prev); next.delete(id); return next; }); await executeOptimistic(id, update.action, update.optimistic, update.rollback); }; return { pending, failed, executeOptimistic, retry }; } // Usage in drag-drop function OptimisticDragDrop({ items, onSave }) { const [localItems, setLocalItems] = useState(items); const { executeOptimistic, failed } = useOptimisticUpdates(); const handleDragEnd = async (event) => { const { active, over } = event; if (!over || active.id === over.id) return; const oldItems = [...localItems]; const newItems = reorderItems(localItems, active.id, over.id); // Apply optimistically setLocalItems(newItems); // Save to server with rollback await executeOptimistic( `reorder-${Date.now()}`, () => onSave(newItems), newItems, oldItems ).catch(() => { // Rollback handled automatically setLocalItems(oldItems); toast.error('Failed to save. Please try again.'); }); }; return ( {failed.size > 0 && (
Some changes failed to save. Retrying...
)} {/* Render items */}
); } ``` ## Undo/Redo Functionality ### History Management ```tsx interface HistoryState { past: T[]; present: T; future: T[]; } function useHistory(initialState: T) { const [history, setHistory] = useState>({ past: [], present: initialState, future: [], }); const canUndo = history.past.length > 0; const canRedo = history.future.length > 0; const push = useCallback((newState: T) => { setHistory(prev => ({ past: [...prev.past, prev.present], present: newState, future: [], // Clear future on new action })); }, []); const undo = useCallback(() => { setHistory(prev => { if (prev.past.length === 0) return prev; const previous = prev.past[prev.past.length - 1]; const newPast = prev.past.slice(0, prev.past.length - 1); return { past: newPast, present: previous, future: [prev.present, ...prev.future], }; }); }, []); const redo = useCallback(() => { setHistory(prev => { if (prev.future.length === 0) return prev; const next = prev.future[0]; const newFuture = prev.future.slice(1); return { past: [...prev.past, prev.present], present: next, future: newFuture, }; }); }, []); const reset = useCallback(() => { setHistory({ past: [], present: initialState, future: [], }); }, [initialState]); // Keyboard shortcuts useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === 'z' && !e.shiftKey) { e.preventDefault(); if (canUndo) undo(); } if ((e.metaKey || e.ctrlKey) && (e.key === 'y' || (e.key === 'z' && e.shiftKey))) { e.preventDefault(); if (canRedo) redo(); } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [canUndo, canRedo, undo, redo]); return { state: history.present, push, undo, redo, reset, canUndo, canRedo, history, }; } // Usage with drag-drop function DragDropWithHistory() { const { state: items, push, undo, redo, canUndo, canRedo } = useHistory(initialItems); const handleDragEnd = (event) => { const newItems = reorderItems(items, event); push(newItems); // Save to history }; return ( <>
{items.map(item => ( ))} ); } ``` ## Persistence Strategies ### Local Storage Persistence ```tsx function useLocalStorageState(key: string, defaultValue: T) { const [state, setState] = useState(() => { try { const item = localStorage.getItem(key); return item ? JSON.parse(item) : defaultValue; } catch { return defaultValue; } }); const setValue = useCallback((value: T | ((prev: T) => T)) => { setState(prev => { const nextState = value instanceof Function ? value(prev) : value; try { localStorage.setItem(key, JSON.stringify(nextState)); } catch (error) { console.error('Failed to save to localStorage:', error); } return nextState; }); }, [key]); // Sync between tabs useEffect(() => { const handleStorageChange = (e: StorageEvent) => { if (e.key === key && e.newValue) { try { setState(JSON.parse(e.newValue)); } catch {} } }; window.addEventListener('storage', handleStorageChange); return () => window.removeEventListener('storage', handleStorageChange); }, [key]); return [state, setValue] as const; } // Auto-save with debouncing function AutoSaveDragDrop() { const [items, setItems] = useLocalStorageState('drag-drop-items', initialItems); const [lastSaved, setLastSaved] = useState(null); const debouncedItems = useDebounce(items, 1000); useEffect(() => { if (debouncedItems !== items) { setLastSaved(new Date()); } }, [debouncedItems]); return (
{lastSaved && (
Last saved: {lastSaved.toLocaleTimeString()}
)} { const newItems = reorderItems(items, event); setItems(newItems); }}> {/* Content */}
); } ``` ### IndexedDB for Large Datasets ```tsx class DragDropStore { private db: IDBDatabase | null = null; private dbName = 'DragDropDB'; private storeName = 'items'; async init() { return new Promise((resolve, reject) => { const request = indexedDB.open(this.dbName, 1); request.onupgradeneeded = (event) => { const db = (event.target as IDBOpenDBRequest).result; if (!db.objectStoreNames.contains(this.storeName)) { db.createObjectStore(this.storeName, { keyPath: 'id' }); } }; request.onsuccess = (event) => { this.db = (event.target as IDBOpenDBRequest).result; resolve(); }; request.onerror = () => reject(request.error); }); } async saveItems(items: Item[]) { if (!this.db) await this.init(); const transaction = this.db!.transaction([this.storeName], 'readwrite'); const store = transaction.objectStore(this.storeName); // Clear existing items await new Promise((resolve, reject) => { const clearRequest = store.clear(); clearRequest.onsuccess = () => resolve(true); clearRequest.onerror = () => reject(clearRequest.error); }); // Add new items for (const item of items) { store.add(item); } return new Promise((resolve, reject) => { transaction.oncomplete = () => resolve(); transaction.onerror = () => reject(transaction.error); }); } async loadItems(): Promise { if (!this.db) await this.init(); const transaction = this.db!.transaction([this.storeName], 'readonly'); const store = transaction.objectStore(this.storeName); return new Promise((resolve, reject) => { const request = store.getAll(); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } } // Hook for IndexedDB persistence function useIndexedDBState(initialItems: Item[]) { const [items, setItems] = useState(initialItems); const [loading, setLoading] = useState(true); const store = useRef(new DragDropStore()); // Load from IndexedDB on mount useEffect(() => { store.current.loadItems() .then(savedItems => { if (savedItems.length > 0) { setItems(savedItems); } }) .finally(() => setLoading(false)); }, []); // Save to IndexedDB on change const saveItems = useCallback(async (newItems: Item[]) => { setItems(newItems); await store.current.saveItems(newItems); }, []); return { items, saveItems, loading }; } ``` ## Multi-User Synchronization ### WebSocket Real-Time Sync ```tsx function useRealtimeDragDrop(roomId: string) { const [items, setItems] = useState([]); const [otherUsers, setOtherUsers] = useState>(new Map()); const ws = useRef(); useEffect(() => { ws.current = new WebSocket(`wss://api.example.com/rooms/${roomId}`); ws.current.onmessage = (event) => { const message = JSON.parse(event.data); switch (message.type) { case 'ITEMS_UPDATE': setItems(message.items); break; case 'USER_DRAGGING': setOtherUsers(prev => new Map(prev).set(message.userId, { item: message.item, position: message.position, color: message.color, })); break; case 'USER_DROPPED': setOtherUsers(prev => { const next = new Map(prev); next.delete(message.userId); return next; }); break; } }; return () => ws.current?.close(); }, [roomId]); const handleDragStart = (event) => { ws.current?.send(JSON.stringify({ type: 'DRAG_START', item: event.active.data.current?.item, position: { x: event.active.rect.current.translated.left, y: event.active.rect.current.translated.top }, })); }; const handleDragEnd = (event) => { const newItems = reorderItems(items, event); // Optimistic update setItems(newItems); // Broadcast change ws.current?.send(JSON.stringify({ type: 'DRAG_END', items: newItems, })); }; return { items, otherUsers, handleDragStart, handleDragEnd, }; } ``` ## Error Recovery ### Error Boundary for Drag-Drop ```tsx class DragDropErrorBoundary extends Component< { children: React.ReactNode; onError?: (error: Error) => void }, { hasError: boolean; error: Error | null } > { constructor(props) { super(props); this.state = { hasError: false, error: null }; } static getDerivedStateFromError(error: Error) { return { hasError: true, error }; } componentDidCatch(error: Error, errorInfo: ErrorInfo) { console.error('Drag-drop error:', error, errorInfo); this.props.onError?.(error); } render() { if (this.state.hasError) { return (

Something went wrong with drag-and-drop

Error details
{this.state.error?.stack}
); } return this.props.children; } } // Wrap drag-drop components function SafeDragDrop({ children }) { return ( { // Log to error tracking service logError(error, { context: 'drag-drop' }); }} > {children} ); } ```