references/complexity-patterns.md
3,292 tokens · o200k_base · 12,933 bytes
Complexity Reduction Patterns
This document provides patterns for reducing cognitive complexity in Langflow React components.
Understanding Complexity
SonarJS Cognitive Complexity
Langflow does not have automated complexity analysis tools. Assess complexity manually using SonarJS cognitive complexity rules:
- Total Complexity: Sum of all functions' complexity in the file
- Max Complexity: Highest single function complexity
What Increases Complexity
| Pattern | Complexity Impact |
|---|---|
if/else | +1 per branch |
| Nested conditions | +1 per nesting level |
switch/case | +1 per case |
for/while/do | +1 per loop |
&&/` | |
| Nested callbacks | +1 per nesting level |
try/catch | +1 per catch |
| Ternary expressions | +1 per nesting |
Pattern 1: Replace Conditionals with Lookup Tables
Before (complexity: ~15):
const getParameterComponent = (field: InputFieldType) => {
if (field.type === "str") {
if (field.multiline) {
return <TextAreaComponent value={field.value} />
} else if (field.password) {
return <PasswordInput value={field.value} />
} else if (field.options?.length) {
return <Dropdown options={field.options} value={field.value} />
} else {
return <InputComponent value={field.value} />
}
}
if (field.type === "int") {
return <IntComponent value={field.value} />
}
if (field.type === "float") {
return <FloatComponent value={field.value} />
}
if (field.type === "bool") {
return <ToggleComponent value={field.value} />
}
if (field.type === "code") {
return <CodeAreaComponent value={field.value} />
}
return null
}
After (complexity: ~3):
// Define lookup table outside component
const FIELD_TYPE_MAP: Record<string, FC<FieldProps>> = {
int: IntComponent,
float: FloatComponent,
bool: ToggleComponent,
code: CodeAreaComponent,
dict: DictComponent,
file: FileComponent,
}
const getStrVariant = (field: InputFieldType): FC<FieldProps> => {
if (field.multiline) return TextAreaComponent
if (field.password) return PasswordInput
if (field.options?.length) return Dropdown
return InputComponent
}
// Clean component logic
const getParameterComponent = (field: InputFieldType) => {
const Component = field.type === "str"
? getStrVariant(field)
: FIELD_TYPE_MAP[field.type]
if (!Component) return null
return <Component value={field.value} />
}
Pattern 2: Use Early Returns
Before (complexity: ~10):
const handleNodeBuild = () => {
if (isAuthenticated) {
if (hasValidFlow) {
if (!isBuilding) {
if (allInputsConnected) {
startBuild()
} else {
showMissingInputsError()
}
} else {
showBuildInProgressWarning()
}
} else {
showInvalidFlowError()
}
} else {
showAuthError()
}
}
After (complexity: ~4):
const handleNodeBuild = () => {
if (!isAuthenticated) {
showAuthError()
return
}
if (!hasValidFlow) {
showInvalidFlowError()
return
}
if (isBuilding) {
showBuildInProgressWarning()
return
}
if (!allInputsConnected) {
showMissingInputsError()
return
}
startBuild()
}
Pattern 3: Extract Complex Conditions
Before (complexity: high):
const canRunFlow = (() => {
if (flow.is_component) {
return false
}
if (!nodes.length) {
return false
}
if (isBuilding) {
return false
}
if (nodes.some((n) => n.data?.node?.error)) {
return false
}
if (
edges.some(
(e) =>
!e.sourceHandle ||
!e.targetHandle ||
(e.data?.isInvalid && e.data.isInvalid === true),
)
) {
return false
}
return true
})()
After (complexity: lower):
// Extract to named functions
const hasValidNodes = (nodes: Node[]) => {
return nodes.length > 0 && !nodes.some((n) => n.data?.node?.error)
}
const hasValidEdges = (edges: Edge[]) => {
return !edges.some(
(e) => !e.sourceHandle || !e.targetHandle || e.data?.isInvalid,
)
}
// Clean main logic
const canRunFlow =
!flow.is_component &&
!isBuilding &&
hasValidNodes(nodes) &&
hasValidEdges(edges)
Pattern 4: Replace Chained Ternaries
Before (complexity: ~5):
const buildStatusIcon = buildStatus === BuildStatus.BUILT
? <CheckCircle className="text-green-500" />
: buildStatus === BuildStatus.BUILDING
? <Loader className="animate-spin text-blue-500" />
: buildStatus === BuildStatus.ERROR
? <XCircle className="text-red-500" />
: <Circle className="text-gray-400" />
After (complexity: ~2):
const BUILD_STATUS_ICONS: Record<BuildStatus, ReactNode> = {
[BuildStatus.BUILT]: <CheckCircle className="text-green-500" />,
[BuildStatus.BUILDING]: <Loader className="animate-spin text-blue-500" />,
[BuildStatus.ERROR]: <XCircle className="text-red-500" />,
[BuildStatus.IDLE]: <Circle className="text-gray-400" />,
}
const buildStatusIcon = BUILD_STATUS_ICONS[buildStatus] ?? BUILD_STATUS_ICONS[BuildStatus.IDLE]
Pattern 5: Flatten Nested Loops
Before (complexity: high):
const getConnectedNodes = (flow: FlowType) => {
const results: ConnectedNode[] = []
for (const node of flow.data.nodes) {
if (node.data?.node?.template) {
for (const [fieldName, field] of Object.entries(node.data.node.template)) {
if (field.type === "str" && field.load_from_db) {
for (const variable of globalVariables) {
if (variable.name === field.value) {
results.push({
nodeId: node.id,
fieldName,
variableName: variable.name,
})
}
}
}
}
}
}
return results
}
After (complexity: lower):
// Use functional approach
const getConnectedNodes = (flow: FlowType) => {
return flow.data.nodes
.filter((node) => node.data?.node?.template)
.flatMap((node) =>
Object.entries(node.data.node.template)
.filter(([, field]) => field.type === "str" && field.load_from_db)
.flatMap(([fieldName, field]) =>
globalVariables
.filter((variable) => variable.name === field.value)
.map((variable) => ({
nodeId: node.id,
fieldName,
variableName: variable.name,
})),
),
)
}
Pattern 6: Extract Event Handler Logic
Before (complexity: high in component):
const FlowCanvas = () => {
const handleNodeDrop = useCallback(
(event: DragEvent) => {
event.preventDefault()
const nodeData = JSON.parse(event.dataTransfer.getData("application/json"))
if (!nodeData || !nodeData.type) return
const position = screenToFlowPosition({
x: event.clientX,
y: event.clientY,
})
// Validate the node type exists
const nodeType = types[nodeData.type]
if (!nodeType) {
setErrorData({ title: "Invalid node type" })
return
}
// Check for duplicates if component
if (nodeData.node?.is_component) {
const existingComponent = nodes.find(
(n) => n.data.node?.display_name === nodeData.node.display_name,
)
if (existingComponent) {
// 20 more lines of duplicate handling...
}
}
// Create the new node
const newNode = buildNodeFromData(nodeData, position)
setNodes((prev) => [...prev, newNode])
// 20 more lines of post-drop logic...
},
[nodes, types, screenToFlowPosition],
)
return <div>...</div>
}
After (complexity: lower):
// Extract to hook
const useNodeDrop = (nodes: Node[], types: Record<string, any>) => {
const validateNodeData = useCallback(
(nodeData: any): boolean => {
if (!nodeData?.type) return false
if (!types[nodeData.type]) return false
return true
},
[types],
)
const checkDuplicateComponent = useCallback(
(nodeData: any): Node | undefined => {
if (!nodeData.node?.is_component) return undefined
return nodes.find(
(n) => n.data.node?.display_name === nodeData.node.display_name,
)
},
[nodes],
)
return { validateNodeData, checkDuplicateComponent }
}
// Component becomes cleaner
const FlowCanvas = () => {
const { validateNodeData, checkDuplicateComponent } = useNodeDrop(nodes, types)
const handleNodeDrop = useCallback(
(event: DragEvent) => {
event.preventDefault()
const nodeData = JSON.parse(event.dataTransfer.getData("application/json"))
if (!validateNodeData(nodeData)) {
setErrorData({ title: "Invalid node type" })
return
}
const duplicate = checkDuplicateComponent(nodeData)
if (duplicate) {
handleDuplicate(duplicate, nodeData)
return
}
const position = screenToFlowPosition({
x: event.clientX,
y: event.clientY,
})
const newNode = buildNodeFromData(nodeData, position)
setNodes((prev) => [...prev, newNode])
},
[validateNodeData, checkDuplicateComponent, screenToFlowPosition],
)
return <div>...</div>
}
Pattern 7: Reduce Boolean Logic Complexity
Before (complexity: ~8):
const isNodeDisabled = !isAuthenticated
|| flow.is_component
|| isBuilding
|| node.data?.node?.frozen
|| (node.data?.node?.error && node.data.node.error !== "")
|| (!hasConnectedInputs && node.type !== "genericNode")
|| (isLocked && !isSuperUser)
After (complexity: ~3):
// Extract meaningful boolean functions
const hasNodeError = (node: Node) => {
return !!node.data?.node?.error && node.data.node.error !== ""
}
const isNodeAccessible = (node: Node, isLocked: boolean, isSuperUser: boolean) => {
if (isLocked && !isSuperUser) return false
if (node.data?.node?.frozen) return false
return true
}
const canInteractWithNode = (node: Node) => {
if (!isAuthenticated) return false
if (flow.is_component) return false
if (isBuilding) return false
if (hasNodeError(node)) return false
if (!isNodeAccessible(node, isLocked, isSuperUser)) return false
if (!hasConnectedInputs && node.type !== "genericNode") return false
return true
}
const isNodeDisabled = !canInteractWithNode(node)
Pattern 8: Simplify useMemo/useCallback Dependencies
Before (complexity: multiple recalculations):
const processedNodes = useMemo(() => {
let result = nodes
if (searchTerm) {
result = result.filter(
(n) =>
n.data?.node?.display_name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
n.data?.type?.toLowerCase().includes(searchTerm.toLowerCase()),
)
}
if (filterByType) {
result = result.filter((n) => n.data?.type === filterByType)
}
if (sortOrder === "name") {
result = [...result].sort((a, b) =>
(a.data?.node?.display_name ?? "").localeCompare(b.data?.node?.display_name ?? ""),
)
} else if (sortOrder === "type") {
result = [...result].sort((a, b) =>
(a.data?.type ?? "").localeCompare(b.data?.type ?? ""),
)
}
return result
}, [nodes, searchTerm, filterByType, sortOrder])
After (complexity: separated concerns):
// Separate filter and sort utilities
const filterNodes = (nodes: Node[], searchTerm: string, filterByType?: string) => {
let result = nodes
if (searchTerm) {
const term = searchTerm.toLowerCase()
result = result.filter(
(n) =>
n.data?.node?.display_name?.toLowerCase().includes(term) ||
n.data?.type?.toLowerCase().includes(term),
)
}
if (filterByType) {
result = result.filter((n) => n.data?.type === filterByType)
}
return result
}
const SORT_COMPARATORS: Record<string, (a: Node, b: Node) => number> = {
name: (a, b) =>
(a.data?.node?.display_name ?? "").localeCompare(b.data?.node?.display_name ?? ""),
type: (a, b) =>
(a.data?.type ?? "").localeCompare(b.data?.type ?? ""),
}
const sortNodes = (nodes: Node[], sortOrder: string) => {
const comparator = SORT_COMPARATORS[sortOrder]
if (!comparator) return nodes
return [...nodes].sort(comparator)
}
// Clean component usage
const filteredNodes = useMemo(
() => filterNodes(nodes, searchTerm, filterByType),
[nodes, searchTerm, filterByType],
)
const processedNodes = useMemo(
() => sortNodes(filteredNodes, sortOrder),
[filteredNodes, sortOrder],
)
Target Metrics After Refactoring
| Metric | Target |
|---|---|
| Total Complexity | < 50 |
| Max Function Complexity | < 30 |
| Function Length | < 30 lines |
| Nesting Depth | <= 3 levels |
| Conditional Chains | <= 3 conditions |