.agents/skills/component-refactoring/references/component-splitting.md
This document provides detailed guidance on splitting large components into smaller, focused components in Langflow.
Split a component when you identify:
{condition && <JSX />} blocksIdentify visual sections and extract each as a component.
// Before: Monolithic component (500+ lines)
const FlowPage = () => {
return (
<div className="flex h-full w-full">
<div className="w-64 border-r">
<input placeholder="Search components..." />
{categories.map((cat) => (
<div key={cat.name}>
<h3>{cat.name}</h3>
{cat.components.map((comp) => (
<div key={comp.name} draggable>
{comp.display_name}
</div>
))}
</div>
))}
</div>
<div className="flex-1">
<ReactFlow nodes={nodes} edges={edges} onConnect={onConnect}>
</ReactFlow>
</div>
{showInspectPanel && (
<div className="w-80 border-l">
{selectedNode && <NodeInspector node={selectedNode} />}
</div>
)}
{showExportModal && <ExportModal />}
{showShareModal && <ShareModal />}
</div>
)
}
// After: Split into focused components (kebab-case, descriptive names — NEVER index.tsx)
// pages/FlowPage/
// flow-page.tsx (orchestration)
// components/
// flow-sidebar.tsx
// flow-canvas.tsx
// flow-inspect-panel.tsx
// flow-modals.tsx
// FlowSidebar.tsx
interface FlowSidebarProps {
categories: Category[]
searchTerm: string
onSearchChange: (term: string) => void
}
const FlowSidebar: FC<FlowSidebarProps> = ({
categories,
searchTerm,
onSearchChange,
}) => {
return (
<div className="w-64 border-r">
<input
placeholder="Search components..."
value={searchTerm}
onChange={(e) => onSearchChange(e.target.value)}
/>
{categories.map((cat) => (
<SidebarCategory key={cat.name} category={cat} />
))}
</div>
)
}
// flow-page.tsx (orchestration only — kebab-case, NOT index.tsx)
const FlowPage = () => {
const { nodes, edges, onConnect } = useFlowState()
const { activeModal, openModal, closeModal } = useModalState()
const [searchTerm, setSearchTerm] = useState("")
return (
<div className="flex h-full w-full">
<FlowSidebar
categories={filteredCategories}
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
/>
<FlowCanvas
nodes={nodes}
edges={edges}
onConnect={onConnect}
/>
{showInspectPanel && (
<FlowInspectPanel selectedNode={selectedNode} />
)}
<FlowModals
activeModal={activeModal}
onClose={closeModal}
/>
</div>
)
}
Extract large conditional rendering blocks.
// Before: Large conditional blocks
const NodeField = ({ field }: { field: InputFieldType }) => {
return (
<div>
{field.show ? (
<div className="field-visible">
{field.load_from_db ? (
<div className="global-variable-badge">
<Badge>{field.value}</Badge>
<Button onClick={() => clearGlobalVariable(field.name)}>
Clear
</Button>
</div>
) : field.type === "str" && field.multiline ? (
<TextAreaComponent
value={field.value}
onChange={(val) => handleChange(field.name, val)}
/>
) : field.type === "str" ? (
<InputComponent
value={field.value}
onChange={(val) => handleChange(field.name, val)}
password={field.password}
/>
) : field.type === "code" ? (
<CodeAreaComponent
value={field.value}
onChange={(val) => handleChange(field.name, val)}
/>
) : (
<GenericInput field={field} onChange={handleChange} />
)}
</div>
) : null}
</div>
)
}
// After: Separate rendering components
const GlobalVariableBadge: FC<{ field: InputFieldType; onClear: () => void }> = ({
field,
onClear,
}) => (
<div className="global-variable-badge">
<Badge>{field.value}</Badge>
<Button onClick={onClear}>Clear</Button>
</div>
)
const FieldInput: FC<{ field: InputFieldType; onChange: FieldChangeHandler }> = ({
field,
onChange,
}) => {
if (field.load_from_db) {
return <GlobalVariableBadge field={field} onClear={() => onChange(field.name, "")} />
}
const Component = getFieldComponent(field)
return <Component value={field.value} onChange={(val) => onChange(field.name, val)} />
}
const NodeField = ({ field }: { field: InputFieldType }) => {
if (!field.show) return null
return (
<div className="field-visible">
<FieldInput field={field} onChange={handleChange} />
</div>
)
}
Extract modals with their trigger logic.
// Before: Multiple modals in one component
const FlowToolbar = () => {
const [showExport, setShowExport] = useState(false)
const [showShare, setShowShare] = useState(false)
const [showDelete, setShowDelete] = useState(false)
const [showApi, setShowApi] = useState(false)
const onExport = async (format: string) => { /* 20 lines */ }
const onShare = async (data: ShareData) => { /* 20 lines */ }
const onDelete = async () => { /* 15 lines */ }
return (
<div>
{showExport && <ExportModal onConfirm={onExport} onClose={() => setShowExport(false)} />}
{showShare && <ShareModal onConfirm={onShare} onClose={() => setShowShare(false)} />}
{showDelete && <DeleteConfirm onConfirm={onDelete} onClose={() => setShowDelete(false)} />}
{showApi && <ApiModal flowId={flowId} onClose={() => setShowApi(false)} />}
</div>
)
}
// After: Modal manager component
// flow-toolbar-modals.tsx
type ToolbarModalType = "export" | "share" | "delete" | "api" | null
interface FlowToolbarModalsProps {
flowId: string
activeModal: ToolbarModalType
onClose: () => void
onSuccess: () => void
}
const FlowToolbarModals: FC<FlowToolbarModalsProps> = ({
flowId,
activeModal,
onClose,
onSuccess,
}) => {
const handleExport = async (format: string) => {
// export logic
onSuccess()
}
const handleShare = async (data: ShareData) => {
// share logic
onSuccess()
}
const handleDelete = async () => {
// delete logic
onSuccess()
}
return (
<>
{activeModal === "export" && (
<ExportModal onConfirm={handleExport} onClose={onClose} />
)}
{activeModal === "share" && (
<ShareModal onConfirm={handleShare} onClose={onClose} />
)}
{activeModal === "delete" && (
<DeleteConfirm onConfirm={handleDelete} onClose={onClose} />
)}
{activeModal === "api" && (
<ApiModal flowId={flowId} onClose={onClose} />
)}
</>
)
}
// Parent component
const FlowToolbar = () => {
const { activeModal, openModal, closeModal } = useModalState()
return (
<div>
<Button onClick={() => openModal("export")}>Export</Button>
<Button onClick={() => openModal("share")}>Share</Button>
<FlowToolbarModals
flowId={flowId}
activeModal={activeModal}
onClose={closeModal}
onSuccess={handleSuccess}
/>
</div>
)
}
Extract repeated item rendering.
// Before: Inline item rendering
const ComponentList = () => {
return (
<div>
{components.map((comp) => (
<div key={comp.name} className="component-item">
<div className="flex items-center gap-2">
{comp.icon && }
<span className="font-medium">{comp.display_name}</span>
{comp.beta && <Badge variant="secondary">Beta</Badge>}
{comp.legacy && <Badge variant="destructive">Legacy</Badge>}
</div>
<p className="text-sm text-muted-foreground">{comp.description}</p>
<div className="flex gap-1">
{comp.output_types?.map((type) => (
<Badge key={type} variant="outline">{type}</Badge>
))}
</div>
<Button
variant="ghost"
size="sm"
onClick={() => handleAddToCanvas(comp)}
>
Add
</Button>
</div>
))}
</div>
)
}
// After: Extracted item component
interface ComponentItemProps {
component: APIClassType
onAdd: (component: APIClassType) => void
}
const ComponentItem: FC<ComponentItemProps> = ({ component, onAdd }) => {
return (
<div className="component-item">
<div className="flex items-center gap-2">
{component.icon && }
<span className="font-medium">{component.display_name}</span>
{component.beta && <Badge variant="secondary">Beta</Badge>}
{component.legacy && <Badge variant="destructive">Legacy</Badge>}
</div>
<p className="text-sm text-muted-foreground">{component.description}</p>
<div className="flex gap-1">
{component.output_types?.map((type) => (
<Badge key={type} variant="outline">{type}</Badge>
))}
</div>
<Button variant="ghost" size="sm" onClick={() => onAdd(component)}>
Add
</Button>
</div>
)
}
const ComponentList = () => {
return (
<div>
{components.map((comp) => (
<ComponentItem
key={comp.display_name}
component={comp}
onAdd={handleAddToCanvas}
/>
))}
</div>
)
}
For components with 2-3 sub-components:
my-component/
my-component.tsx # Main component (kebab-case, descriptive — NOT index.tsx)
sub-component-a.tsx
sub-component-b.tsx
my-component-types.ts # Shared types
For components with many sub-components:
my-component/
my-component.tsx # Main orchestration (NOT index.tsx)
my-component-types.ts # Shared types
hooks/
use-feature-a.ts
use-feature-b.ts
components/
header-section.tsx
content-section.tsx
modals-section.tsx
helpers/
format-data.ts
IMPORTANT: Never use
index.tsxfor new files. This is a legacy pattern. Use kebab-case file names that describe the component's purpose.
Pages follow a standard directory layout with sub-pages, components, hooks, and helpers:
pages/SettingsPage/
settings-page.tsx # Main page component
pages/ # Sub-pages
ApiKeysPage/
api-keys-page.tsx
components/
api-key-header.tsx
helpers/
column-defs.ts
get-modal-props.tsx
GlobalVariablesPage/
global-variables-page.tsx
components/ # Shared page components
hooks/ # Page-level hooks
utils/ # Page-level utilities
components/ui/
button.tsx
input.tsx
badge.tsx
dialog.tsx
popover.tsx
select.tsx
textarea.tsx
tooltip.tsx
dropdown-menu.tsx
The existing codebase has index.tsx and mixed naming. When refactoring, migrate toward the new kebab-case standard:
// Legacy (existing — DO NOT create new files this way)
components/core/appHeaderComponent/index.tsx
components/common/loadingComponent/index.tsx
CustomNodes/GenericNode/index.tsx
// New standard (use this for all new code and refactors)
components/core/app-header/app-header.tsx
components/common/loading-indicator/loading-indicator.tsx
CustomNodes/GenericNode/generic-node.tsx
NodeName/
NodeOutputField/
NodeStatus/
Pass only what is needed:
// Bad: Passing entire objects when only some fields needed
<NodeHeader nodeData={nodeData} flowData={flowData} />
// Good: Destructure to minimum required
<NodeHeader
displayName={nodeData.node?.display_name ?? ""}
nodeType={nodeData.type}
isFrozen={nodeData.node?.frozen ?? false}
onNameChange={handleNameChange}
/>
Use callbacks for child-to-parent communication:
// Parent
const GenericNode = () => {
const [showDescription, setShowDescription] = useState(false)
return (
<div>
<NodeHeader
displayName={data.node?.display_name ?? ""}
onToggleDescription={() => setShowDescription((prev) => !prev)}
/>
{showDescription && (
<NodeDescription
description={data.node?.description ?? ""}
onChange={handleDescriptionChange}
/>
)}
</div>
)
}
// Child
interface NodeHeaderProps {
displayName: string
onToggleDescription: () => void
}
const NodeHeader: FC<NodeHeaderProps> = ({ displayName, onToggleDescription }) => {
return (
<div className="flex items-center justify-between">
<span>{displayName}</span>
<button onClick={onToggleDescription}>Toggle Description</button>
</div>
)
}
When sub-components need parent context:
interface FieldListProps<T> {
fields: T[]
renderField: (field: T, index: number) => React.ReactNode
renderEmpty?: () => React.ReactNode
}
function FieldList<T>({ fields, renderField, renderEmpty }: FieldListProps<T>) {
if (fields.length === 0 && renderEmpty) {
return <>{renderEmpty()}</>
}
return (
<div className="flex flex-col gap-2">
{fields.map((field, index) => renderField(field, index))}
</div>
)
}
// Usage
<FieldList
fields={visibleFields}
renderField={(field, i) => (
<NodeInputField key={field.name ?? i} field={field} onChange={handleChange} />
)}
renderEmpty={() => <span className="text-muted-foreground">No fields</span>}
/>
The GenericNode component is one of the most complex components in Langflow. When splitting:
index.tsx as an orchestrator that composes sub-components.components/NodeInputField/.components/HandleRenderComponent/.components/NodeStatus/.The flow editor page has multiple distinct regions:
Each region should be its own component with clearly defined props.
When a component reads from multiple Zustand stores:
// Parent: reads from stores
const FlowToolbar = () => {
const isBuilding = useFlowStore((state) => state.isBuilding)
const currentFlow = useFlowsManagerStore((state) => state.currentFlow)
const isAuthenticated = useAuthStore((state) => state.isAuthenticated)
return (
<ToolbarActions
isBuilding={isBuilding}
flowName={currentFlow?.name ?? ""}
canBuild={isAuthenticated && !isBuilding}
onBuild={handleBuild}
onSave={handleSave}
/>
)
}
// Child: pure presentational
const ToolbarActions: FC<ToolbarActionsProps> = ({
isBuilding,
flowName,
canBuild,
onBuild,
onSave,
}) => {
return (
<div className="flex items-center gap-2">
<span>{flowName}</span>
<Button onClick={onBuild} disabled={!canBuild}>
{isBuilding ? "Building..." : "Build"}
</Button>
<Button onClick={onSave}>Save</Button>
</div>
)
}