12 KiB
WorkflowUI Canvas Hook Refactoring - COMPLETE
Status: ✓ COMPLETE
Date: 2026-01-23
Author: Claude Code (AI Assistant)
Breaking Changes: NONE (100% backward compatible)
Executive Summary
The monolithic useProjectCanvas.ts hook (322 LOC) has been successfully refactored into 8 focused, modular hooks, each under 150 LOC. The refactoring maintains complete backward compatibility while providing a more maintainable, testable, and flexible API.
Key Metrics
| Metric | Before | After | Status |
|---|---|---|---|
| Total Files | 1 | 8 | ✓ +7 focused files |
| Max File Size | 322 LOC | 145 LOC | ✓ -55% reduction |
| Min File Size | 322 LOC | 40 LOC | ✓ Granular control |
| Avg File Size | 322 LOC | 83 LOC | ✓ -74% average |
| Compliance | FAIL (>150) | PASS (all ≤150) | ✓ Full compliance |
| Breaking Changes | N/A | 0 | ✓ Safe migration |
File Structure
New Directory: /src/hooks/canvas/
src/hooks/canvas/
├── index.ts (145 LOC) - Composition
├── useCanvasZoom.ts (52 LOC) - Zoom control
├── useCanvasPan.ts (52 LOC) - Pan/drag state
├── useCanvasSettings.ts (55 LOC) - Grid settings
├── useCanvasSelection.ts (85 LOC) - Selection mgmt
├── useCanvasItems.ts (121 LOC) - Load/delete items
├── useCanvasItemsOperations.ts (113 LOC) - Create/update items
└── useCanvasGridUtils.ts (40 LOC) - Grid math
Modified Files
/src/hooks/index.ts- Updated exports to include new hooks/src/hooks/useProjectCanvas.ts.old- Original file (archived for reference)
Detailed Hook Breakdown
1. useCanvasZoom (52 LOC)
Purpose: Manage canvas viewport zoom level
export interface UseCanvasZoomReturn {
zoom: number; // Current zoom (0.1 to 3.0)
zoomIn: () => void; // Increase by 1.2x
zoomOut: () => void; // Decrease by 1.2x
resetView: () => void; // Reset to 1.0
setZoom: (zoom: number) => void; // Set specific level
}
Redux Slices Used: canvasSlice
2. useCanvasPan (52 LOC)
Purpose: Manage canvas panning and dragging state
export interface UseCanvasPanReturn {
pan: CanvasPosition; // { x, y } offset
isDragging: boolean; // Current drag state
panTo: (position: CanvasPosition) => void; // Absolute position
panBy: (delta: CanvasPosition) => void; // Relative movement
setDraggingState: (isDragging: boolean) => void; // Control drag state
}
Redux Slices Used: canvasSlice
3. useCanvasSettings (55 LOC)
Purpose: Manage grid and snap settings
export interface UseCanvasSettingsReturn {
gridSnap: boolean; // Snap-to-grid enabled
showGrid: boolean; // Grid display enabled
snapSize: number; // Grid size (pixels)
toggleGridSnap: () => void; // Toggle snap behavior
toggleShowGrid: () => void; // Toggle grid display
setSnapSizeValue: (size: number) => void; // Set grid size
}
Redux Slices Used: canvasSlice
4. useCanvasSelection (85 LOC)
Purpose: Manage item selection
export interface UseCanvasSelectionReturn {
selectedItemIds: string[]; // Selected IDs
selectedItems: ProjectCanvasItem[]; // Resolved items
selectItem: (itemId: string) => void; // Single select
addToSelection: (itemId: string) => void; // Add item
removeFromSelection: (itemId: string) => void; // Remove item
toggleSelection: (itemId: string) => void; // Toggle item
setSelectionIds: (itemIds: string[]) => void; // Set selection
clearSelection: () => void; // Deselect all
selectAllItems: () => void; // Select all
}
Redux Slices Used: canvasSlice, canvasItemsSlice
Note: Automatically resolves selected IDs to full item objects
5. useCanvasItems (121 LOC)
Purpose: Load and delete canvas items
export interface UseCanvasItemsReturn {
canvasItems: ProjectCanvasItem[]; // All items
isLoading: boolean; // Loading state
error: string | null; // Error message
isResizing: boolean; // Resize state
loadCanvasItems: () => Promise<void>; // Fetch from server
deleteCanvasItem: (itemId: string) => Promise<void>; // Delete item
setResizingState: (isResizing: boolean) => void; // Control resize
}
Redux Slices Used: projectSlice, canvasSlice, canvasItemsSlice
Lifecycle: Auto-loads items when projectId changes
Storage: Server (projectService) + IndexedDB cache
6. useCanvasItemsOperations (113 LOC)
Purpose: Create, update, and bulk-update items
export interface UseCanvasItemsOperationsReturn {
createCanvasItem: (data: CreateCanvasItemRequest) => Promise<ProjectCanvasItem | null>;
updateCanvasItem: (itemId: string, data: UpdateCanvasItemRequest) => Promise<ProjectCanvasItem | null>;
bulkUpdateItems: (updates: Array<Partial<ProjectCanvasItem> & { id: string }>) => Promise<void>;
}
Redux Slices Used: projectSlice, canvasItemsSlice
Storage: Server (projectService) + IndexedDB cache
7. useCanvasGridUtils (40 LOC)
Purpose: Grid utility functions
export interface UseCanvasGridUtilsReturn {
snapToGrid: (position: { x: number; y: number }) => { x: number; y: number };
}
Redux Slices Used: canvasSlice
Pure Function: No side effects, just math
8. index.ts Composition Hook (145 LOC)
Purpose: Compose all 7 hooks into single useProjectCanvas() interface
export function useProjectCanvas(): UseProjectCanvasReturn {
// Structured API (new, recommended)
const zoomHook = useCanvasZoom();
const panHook = useCanvasPan();
const selectionHook = useCanvasSelection();
const itemsHook = useCanvasItems();
const settingsHook = useCanvasSettings();
const operationsHook = useCanvasItemsOperations();
const gridUtilsHook = useCanvasGridUtils();
return {
// Provide both structured AND flattened APIs
zoomHook, panHook, selectionHook, itemsHook, settingsHook,
operationsHook, gridUtilsHook,
// Backward compatible snake_case API
zoom, zoom_in, zoom_out, reset_view,
pan, pan_canvas, set_dragging,
gridSnap, showGrid, snapSize, toggle_grid_snap, toggle_show_grid,
select_item, select_add, select_remove, select_toggle, select_clear,
canvasItems, selectedItemIds, selectedItems,
// ... etc
}
}
Redux Integration
Slice Distribution
Before (Incorrect):
- All canvas actions imported from
projectSlice
After (Corrected):
canvasSlice- Zoom, pan, selection, grid settings, drag/resize statecanvasItemsSlice- Canvas items CRUD operationsprojectSlice- Project state, loading, errors
Import Mapping
| Action/Selector | Original Slice | Corrected Slice |
|---|---|---|
setCanvasZoom |
projectSlice ❌ | canvasSlice ✓ |
setCanvasPan |
projectSlice ❌ | canvasSlice ✓ |
selectCanvasZoom |
projectSlice ❌ | canvasSlice ✓ |
selectCanvasItem |
projectSlice ❌ | canvasSlice ✓ |
setCanvasItems |
projectSlice ❌ | canvasItemsSlice ✓ |
addCanvasItem |
projectSlice ❌ | canvasItemsSlice ✓ |
selectCanvasItems |
projectSlice ❌ | canvasItemsSlice ✓ |
setLoading |
projectSlice ✓ | projectSlice ✓ |
selectCurrentProjectId |
projectSlice ✓ | projectSlice ✓ |
Backward Compatibility
API Parity
All original methods remain available with identical signatures:
// Original usage (STILL WORKS)
const canvas = useProjectCanvas();
// Zoom
canvas.zoom_in();
canvas.zoom_out();
canvas.reset_view();
// Pan
canvas.pan_canvas({ x: 10, y: 20 });
canvas.set_dragging(true);
// Selection
canvas.select_item('item-123');
canvas.select_add('item-456');
canvas.select_clear();
canvas.select_all_items();
// Settings
canvas.toggle_grid_snap();
canvas.toggle_show_grid();
canvas.set_snap_size(25);
// Items
canvas.loadCanvasItems();
canvas.createCanvasItem(data);
canvas.updateCanvasItem('id', data);
canvas.deleteCanvasItem('id');
canvas.bulkUpdateItems(updates);
// Utilities
canvas.snap_to_grid({ x: 100, y: 200 });
// State
const { zoom, pan, selectedItems, canvasItems, isLoading } = canvas;
Recommended New Usage
// Structured API (new, more maintainable)
const canvas = useProjectCanvas();
// Zoom operations
canvas.zoomHook.zoomIn();
canvas.zoomHook.zoomOut();
// Selection operations
canvas.selectionHook.selectItem('item-123');
canvas.selectionHook.addToSelection('item-456');
// Grid utilities
canvas.gridUtilsHook.snapToGrid({ x: 100, y: 200 });
// Items operations
canvas.itemsHook.loadCanvasItems();
canvas.operationsHook.createCanvasItem(data);
// State access
const { zoom } = canvas.zoomHook;
const { selectedItems } = canvas.selectionHook;
const { canvasItems } = canvas.itemsHook;
Direct Hook Usage
// Most flexible - import only what you need
import { useCanvasZoom, useCanvasSelection } from '../hooks/canvas';
const { zoomIn, zoomOut } = useCanvasZoom();
const { selectItem, addToSelection } = useCanvasSelection();
zoomIn();
selectItem('item-123');
Testing Strategy
Unit Tests
- Test each hook independently
- Mock Redux store
- Verify state transitions
- Validate action dispatches
Integration Tests
- Test hook composition
- Verify backward compatibility
- Test Redux flow end-to-end
Type Safety
- TypeScript strict mode
- Explicit interfaces for each hook
- Proper action/selector typing
Performance Considerations
Optimization Benefits
- Granular Re-renders: Components using only
useCanvasZoomwon't re-render when selection changes - Selective Imports: Tree-shaking will eliminate unused hooks
- Lazy Initialization: Hooks initialize selectors only when used
Memoization
All callbacks properly memoized with dependency arrays:
const zoomIn = useCallback(() => {
dispatch(setCanvasZoom(Math.min(zoom * 1.2, 3)));
}, [zoom, dispatch]); // ✓ Proper dependencies
Migration Path
Phase 1: Deploy (No Changes Required)
- Deploy new hooks
- Existing code continues to work
- Old
useProjectCanvas.tscan be removed
Phase 2: Gradual Modernization (Optional)
- Components gradually adopt new
zoomHook,panHookAPI - No pressure - backward compat maintained indefinitely
Phase 3: Future Cleanup (Optional)
- Remove flattened API from composition hook
- Update all components to use structured API
- Simplify types and interfaces
Files Changed
Created
✓ /src/hooks/canvas/index.ts
✓ /src/hooks/canvas/useCanvasZoom.ts
✓ /src/hooks/canvas/useCanvasPan.ts
✓ /src/hooks/canvas/useCanvasSelection.ts
✓ /src/hooks/canvas/useCanvasItems.ts
✓ /src/hooks/canvas/useCanvasItemsOperations.ts
✓ /src/hooks/canvas/useCanvasSettings.ts
✓ /src/hooks/canvas/useCanvasGridUtils.ts
Modified
✓ /src/hooks/index.ts - Updated exports
Archived
✓ /src/hooks/useProjectCanvas.ts.old
Verification Checklist
- All files created under 150 LOC
- Code-only LOC (without comments) under 110 for most files
- Redux imports corrected to proper slices
- TypeScript compilation passes (except unrelated realtimeSlice warning)
- Backward compatibility maintained (all original methods available)
- New structured API provided (zoomHook, panHook, etc.)
- Proper type definitions for all hooks
- Dependencies properly tracked in useCallback dependencies
- No breaking changes to existing code
- All hooks properly exported from index.ts
Summary
✓ 322 LOC monolith → 8 focused hooks (max 145 LOC)
✓ Proper separation of concerns
✓ Redux slices correctly mapped
✓ 100% backward compatible
✓ Cleaner, more testable code
✓ Type-safe interfaces
✓ Ready for production
The refactoring is complete and ready for integration.