π§ Quick Reference: Workflow UX Patterns
For developers working with ValkyrAI Workflow Studio
π― Common Patternsβ
Pattern 1: Prevent Re-render Loops in Draggable Componentsβ
const [isDragging, setIsDragging] = useState(false);
const [hasInitialized, setHasInitialized] = useState(false);
// Initialize once
useEffect(() => {
if (!hasInitialized) {
// Setup code here
setHasInitialized(true);
}
}, [state.id]); // Only on ID change
// Block updates during user interaction
useEffect(() => {
if (isDragging || isResizing) {
return; // Don't fight user input
}
// Position/size updates here
}, [externalState, isDragging, isResizing]);
Pattern 2: Connect Component to Redux Workflowβ
import { useAppSelector } from "@/redux/hooks";
import { selectWorkflowDraft } from "@/redux/features/workflows";
const MyComponent = ({ tasks: propTasks }) => {
// Get from Redux
const workflowDraft = useAppSelector(selectWorkflowDraft);
// Priority: props > Redux > API
const sourceTasks = propTasks || workflowDraft?.tasks || [];
// Use sourceTasks for rendering
};
Pattern 3: Skip Unnecessary API Queriesβ
const { data, isLoading } = useGetTasksQuery(undefined, {
skip: !!(propTasks || workflowDraft?.tasks), // Skip if we have local data
});
Pattern 4: Enhanced Drag Ghostβ
const onDragStart = (event: React.DragEvent, item: any) => {
// Create enhanced drag image
const dragImage = event.currentTarget.cloneNode(true) as HTMLElement;
dragImage.style.opacity = "0.8";
dragImage.style.transform = "scale(1.05)";
dragImage.style.pointerEvents = "none";
document.body.appendChild(dragImage);
event.dataTransfer.setDragImage(dragImage, 50, 25);
// Cleanup
setTimeout(() => document.body.removeChild(dragImage), 0);
// Set data
event.dataTransfer.setData("application/reactflow", JSON.stringify(item));
event.dataTransfer.effectAllowed = "move";
};
π« Anti-Patterns (Avoid These)β
β Don't: Update position from multiple sourcesβ
// BAD - Circular dependency
useEffect(() => {
setPosition(...);
}, [position, size]); // position affects size affects position
useEffect(() => {
setSize(...);
}, [position]); // LOOP!
β Do: Single source of truthβ
// GOOD - One-way flow
useEffect(() => {
if (!isDragging) {
setPosition(clampPosition(externalPosition, size));
}
}, [externalPosition, isDragging]);
β Don't: Update state during renderβ
// BAD
const MyComponent = () => {
if (condition) {
setState(newValue); // Error!
}
return <div />;
};
β Do: Update state in effectsβ
// GOOD
const MyComponent = () => {
useEffect(() => {
if (condition) {
setState(newValue);
}
}, [condition]);
return <div />;
};
π¨ CSS Best Practicesβ
GPU-Accelerated Transformsβ
.draggable-item {
/* Use transform, not top/left for animation */
transform: translateY(-2px) scale(1.02);
/* Hint to browser */
will-change: transform, box-shadow;
/* Smooth easing */
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
/* Prevent flicker */
backface-visibility: hidden;
}
Cursor Feedbackβ
.palette-item {
cursor: grab;
user-select: none; /* Prevent text selection */
}
.palette-item:active {
cursor: grabbing;
}
π Debugging Tipsβ
Check for Re-render Loopsβ
// Browser console
let renderCount = 0;
// In component
useEffect(() => {
renderCount++;
console.log("Render count:", renderCount);
});
// If renderCount > 20 in 2 seconds = problem
Verify Redux Connectionβ
// Browser console
import { store } from "@/redux/store";
// Check workflow state
console.log(store.getState().workflowEditor.draft);
// Check canvas state
console.log(store.getState().workflowCanvas.present.nodes);
Profile Performanceβ
// Chrome DevTools β Performance
// Click Record
// Perform drag operation
// Stop recording
// Look for:
// - Long tasks (> 50ms)
// - Excessive re-renders
// - Memory spikes
π Performance Targetsβ
| Metric | Target | Critical |
|---|---|---|
| Drag FPS | 60 | 30 |
| State updates/sec | < 10 | < 50 |
| Render time | < 16ms | < 50ms |
| Memory growth | < 5MB/min | < 20MB/min |
π§ͺ Quick Test Commandsβ
# Type check
yarn tsc --noEmit
# Lint
yarn lint
# Build (checks for errors)
yarn build
# Dev server
yarn dev
π Code Review Checklistβ
When reviewing drag/workflow code:
- No state updates in render phase
- useEffect dependencies correct
- No circular dependencies
- Redux selectors memoized
- API queries conditionally skipped
- CSS uses transforms (not top/left)
- Drag handlers cleanup properly
- No console errors/warnings
π Related Filesβ
State Managementβ
redux/features/workflows/workflowEditorSlice.ts- Workflow stateredux/features/workflows/workflowCanvasSlice.ts- Canvas state
Componentsβ
components/Dashboard/DraggableFloatingToolbar.tsx- Drag patternwebsite-aurora/app/workflow/Workflow3DViewer.tsx- Redux connectioncomponents/WorkflowStudio/FloatingExecModulesToolbar.tsx- Drag ghost
Docsβ
N8N_KILLER_UX_UPDATE.md- Feature overviewWORKFLOW_UX_FIXES_OCT25.md- Fix detailsTESTING_WORKFLOW_UX_FIXES.md- Testing guide
π‘ Pro Tipsβ
- Always guard user interactions: Check
isDraggingbefore external updates - Use refs for transient state: Position during drag doesn't need re-render
- Memoize expensive computations: Use
useMemofor derived data - Batch state updates: Use
unstable_batchedUpdatesif needed - Profile early: Use React DevTools Profiler during development
π Common Issues & Solutionsβ
Issue: "Maximum update depth exceeded"β
Solution: Add guards to prevent circular dependencies
if (isDragging || isResizing) return;
Issue: 3D viewer not updatingβ
Solution: Connect to Redux
const workflowDraft = useAppSelector(selectWorkflowDraft);
Issue: Drag feels laggyβ
Solution: Reduce state updates, use transforms
transform: translate(${x}px, ${y}px);
/* Not: top: ${y}px; left: ${x}px; */
Keep this reference handy when working with workflow UX components!
Questions? Check the full docs in WORKFLOW_UX_FIXES_OCT25.md