Skip to main content

πŸ”§ 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​

MetricTargetCritical
Drag FPS6030
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

State Management​

  • redux/features/workflows/workflowEditorSlice.ts - Workflow state
  • redux/features/workflows/workflowCanvasSlice.ts - Canvas state

Components​

  • components/Dashboard/DraggableFloatingToolbar.tsx - Drag pattern
  • website-aurora/app/workflow/Workflow3DViewer.tsx - Redux connection
  • components/WorkflowStudio/FloatingExecModulesToolbar.tsx - Drag ghost

Docs​

  • N8N_KILLER_UX_UPDATE.md - Feature overview
  • WORKFLOW_UX_FIXES_OCT25.md - Fix details
  • TESTING_WORKFLOW_UX_FIXES.md - Testing guide

πŸ’‘ Pro Tips​

  1. Always guard user interactions: Check isDragging before external updates
  2. Use refs for transient state: Position during drag doesn't need re-render
  3. Memoize expensive computations: Use useMemo for derived data
  4. Batch state updates: Use unstable_batchedUpdates if needed
  5. 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