mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-25 14:25:02 +00:00
- codegen: Low-code React app with JSON-driven component system - packagerepo: Schema-driven package repository with backend/frontend - postgres: Next.js app with Drizzle ORM and PostgreSQL Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
14 KiB
14 KiB
Refactoring Example: Breaking Down Large Components
This guide demonstrates how to refactor a monolithic component (FeatureIdeaCloud, ~500 LOC) into smaller, maintainable pieces using the hook library and JSON orchestration.
Before: Monolithic Component (500+ LOC)
The original FeatureIdeaCloud.tsx contains:
- State management for ideas, groups, and connections
- ReactFlow canvas logic
- Dialog management for creating/editing ideas
- AI generation logic
- Connection validation
- All UI rendering
Problems:
- Hard to test individual pieces
- Difficult to modify without breaking other parts
- Can't reuse logic in other components
- Takes time to understand the entire component
After: Modular Architecture (<150 LOC each)
Step 1: Extract Data Management Hook
// src/hooks/feature-ideas/use-idea-manager.ts (60 LOC)
import { useKV } from '@github/spark/hooks'
import { useCallback } from 'react'
export interface FeatureIdea {
id: string
title: string
description: string
category: string
priority: 'low' | 'medium' | 'high'
status: 'idea' | 'planned' | 'in-progress' | 'completed'
createdAt: number
parentGroup?: string
}
export interface IdeaGroup {
id: string
label: string
color: string
createdAt: number
}
export interface IdeaConnection {
id: string
from: string
to: string
createdAt: number
}
export function useIdeaManager() {
const [ideas, setIdeas] = useKV<FeatureIdea[]>('feature-ideas', [])
const [groups, setGroups] = useKV<IdeaGroup[]>('idea-groups', [])
const [connections, setConnections] = useKV<IdeaConnection[]>('idea-connections', [])
const addIdea = useCallback((idea: FeatureIdea) => {
setIdeas(current => [...current, idea])
}, [setIdeas])
const updateIdea = useCallback((id: string, updates: Partial<FeatureIdea>) => {
setIdeas(current =>
current.map(idea => idea.id === id ? { ...idea, ...updates } : idea)
)
}, [setIdeas])
const deleteIdea = useCallback((id: string) => {
setIdeas(current => current.filter(idea => idea.id !== id))
setConnections(current =>
current.filter(conn => conn.from !== id && conn.to !== id)
)
}, [setIdeas, setConnections])
return {
ideas,
groups,
connections,
addIdea,
updateIdea,
deleteIdea,
}
}
Step 2: Extract Canvas Hook
// src/hooks/feature-ideas/use-idea-canvas.ts (80 LOC)
import { useNodesState, useEdgesState, Node, Edge } from 'reactflow'
import { useCallback, useEffect } from 'react'
import { FeatureIdea, IdeaConnection } from './use-idea-manager'
export function useIdeaCanvas(ideas: FeatureIdea[], connections: IdeaConnection[]) {
const [nodes, setNodes, onNodesChange] = useNodesState([])
const [edges, setEdges, onEdgesChange] = useEdgesState([])
useEffect(() => {
const newNodes: Node[] = ideas.map(idea => ({
id: idea.id,
type: 'ideaNode',
position: { x: 0, y: 0 },
data: { idea }
}))
setNodes(newNodes)
}, [ideas, setNodes])
useEffect(() => {
const newEdges: Edge[] = connections.map(conn => ({
id: conn.id,
source: conn.from,
target: conn.to,
type: 'smoothstep'
}))
setEdges(newEdges)
}, [connections, setEdges])
const updateNodePosition = useCallback((nodeId: string, position: { x: number; y: number }) => {
setNodes(current =>
current.map(node =>
node.id === nodeId ? { ...node, position } : node
)
)
}, [setNodes])
return {
nodes,
edges,
onNodesChange,
onEdgesChange,
updateNodePosition
}
}
Step 3: Extract Connection Logic Hook
// src/hooks/feature-ideas/use-idea-connections.ts (70 LOC)
import { useCallback } from 'react'
import { Connection as RFConnection } from 'reactflow'
import { IdeaConnection } from './use-idea-manager'
export function useIdeaConnections(
connections: IdeaConnection[],
onAdd: (conn: IdeaConnection) => void,
onRemove: (id: string) => void
) {
const canConnect = useCallback((from: string, to: string) => {
if (from === to) return false
const existingConnection = connections.find(
conn => conn.from === from && conn.to === to
)
return !existingConnection
}, [connections])
const addConnection = useCallback((connection: RFConnection) => {
if (!canConnect(connection.source, connection.target)) {
return false
}
const newConnection: IdeaConnection = {
id: `${connection.source}-${connection.target}`,
from: connection.source,
to: connection.target,
createdAt: Date.now()
}
onAdd(newConnection)
return true
}, [canConnect, onAdd])
const removeConnection = useCallback((id: string) => {
onRemove(id)
}, [onRemove])
return {
canConnect,
addConnection,
removeConnection
}
}
Step 4: Create Small UI Components
// src/components/feature-ideas/IdeaNode.tsx (50 LOC)
import { memo } from 'react'
import { Handle, Position } from 'reactflow'
import { Card } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { FeatureIdea } from '@/hooks/feature-ideas/use-idea-manager'
interface IdeaNodeProps {
idea: FeatureIdea
onEdit: () => void
onDelete: () => void
}
export const IdeaNode = memo(({ idea, onEdit, onDelete }: IdeaNodeProps) => {
return (
<Card className="p-3 min-w-[200px]">
<Handle type="target" position={Position.Left} />
<div className="flex items-start justify-between gap-2 mb-2">
<h3 className="font-semibold text-sm">{idea.title}</h3>
<Badge variant={getPriorityVariant(idea.priority)}>
{idea.priority}
</Badge>
</div>
<p className="text-xs text-muted-foreground mb-2">
{idea.description}
</p>
<div className="flex gap-1">
<button onClick={onEdit} className="text-xs">Edit</button>
<button onClick={onDelete} className="text-xs text-destructive">
Delete
</button>
</div>
<Handle type="source" position={Position.Right} />
</Card>
)
})
function getPriorityVariant(priority: string) {
switch (priority) {
case 'high': return 'destructive'
case 'medium': return 'default'
default: return 'secondary'
}
}
// src/components/feature-ideas/IdeaToolbar.tsx (40 LOC)
import { Button } from '@/components/ui/button'
import { Plus, Sparkle } from '@phosphor-icons/react'
interface IdeaToolbarProps {
onAddIdea: () => void
onGenerateAI: () => void
ideaCount: number
}
export function IdeaToolbar({ onAddIdea, onGenerateAI, ideaCount }: IdeaToolbarProps) {
return (
<div className="flex items-center justify-between p-4 border-b">
<div>
<h2 className="text-lg font-semibold">Feature Ideas</h2>
<p className="text-sm text-muted-foreground">
{ideaCount} ideas
</p>
</div>
<div className="flex gap-2">
<Button onClick={onAddIdea} variant="outline">
<Plus className="mr-2" size={16} />
Add Idea
</Button>
<Button onClick={onGenerateAI}>
<Sparkle className="mr-2" size={16} />
AI Generate
</Button>
</div>
</div>
)
}
Step 5: Compose Main Component
// src/components/FeatureIdeaCloud.tsx (120 LOC)
import ReactFlow, { Background, Controls } from 'reactflow'
import 'reactflow/dist/style.css'
import { useIdeaManager } from '@/hooks/feature-ideas/use-idea-manager'
import { useIdeaCanvas } from '@/hooks/feature-ideas/use-idea-canvas'
import { useIdeaConnections } from '@/hooks/feature-ideas/use-idea-connections'
import { useDialog } from '@/hooks/ui/use-dialog'
import { IdeaNode } from './feature-ideas/IdeaNode'
import { IdeaToolbar } from './feature-ideas/IdeaToolbar'
import { IdeaDialog } from './feature-ideas/IdeaDialog'
const nodeTypes = {
ideaNode: IdeaNode
}
export function FeatureIdeaCloud() {
const {
ideas,
connections,
addIdea,
updateIdea,
deleteIdea
} = useIdeaManager()
const {
nodes,
edges,
onNodesChange,
onEdgesChange
} = useIdeaCanvas(ideas, connections)
const {
addConnection,
removeConnection
} = useIdeaConnections(connections, addIdea, deleteIdea)
const { isOpen, open, close } = useDialog()
return (
<div className="h-full flex flex-col">
<IdeaToolbar
onAddIdea={open}
onGenerateAI={handleAIGenerate}
ideaCount={ideas.length}
/>
<div className="flex-1">
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={addConnection}
nodeTypes={nodeTypes}
>
<Background />
<Controls />
</ReactFlow>
</div>
<IdeaDialog
open={isOpen}
onClose={close}
onSave={addIdea}
/>
</div>
)
}
async function handleAIGenerate() {
// AI generation logic
}
Step 6: Define as JSON (Optional)
{
"id": "feature-idea-cloud",
"name": "Feature Idea Cloud",
"description": "Visual brainstorming canvas for features",
"layout": {
"type": "single"
},
"components": [
{
"id": "toolbar",
"type": "IdeaToolbar",
"bindings": [
{
"source": "ideas.length",
"target": "ideaCount"
}
],
"events": [
{
"event": "onAddIdea",
"action": "open-dialog"
},
{
"event": "onGenerateAI",
"action": "ai-generate-ideas"
}
]
},
{
"id": "canvas",
"type": "IdeaCanvas",
"bindings": [
{
"source": "ideas",
"target": "ideas"
},
{
"source": "connections",
"target": "connections"
}
]
}
],
"hooks": [
{
"id": "ideas",
"name": "useIdeaManager",
"exports": ["ideas", "connections", "addIdea", "updateIdea", "deleteIdea"]
},
{
"id": "dialog",
"name": "useDialog",
"exports": ["isOpen", "open", "close"]
}
],
"actions": [
{
"id": "open-dialog",
"type": "custom",
"handler": "open"
},
{
"id": "ai-generate-ideas",
"type": "ai-generate",
"params": {
"prompt": "Generate 5 creative feature ideas for a project management tool",
"target": "Ideas"
}
}
]
}
Benefits Achieved
1. Component Size Reduction
| Component | Before | After |
|---|---|---|
| FeatureIdeaCloud | 500 LOC | 120 LOC |
| Hooks | 0 | 210 LOC (3 files) |
| UI Components | 0 | 90 LOC (2 files) |
2. Testability
// Easy to test individual hooks
describe('useIdeaManager', () => {
it('should add idea', () => {
const { result } = renderHook(() => useIdeaManager())
act(() => {
result.current.addIdea(mockIdea)
})
expect(result.current.ideas).toHaveLength(1)
})
})
// Easy to test UI components
describe('IdeaNode', () => {
it('should call onEdit when edit clicked', () => {
const onEdit = jest.fn()
const { getByText } = render(<IdeaNode idea={mockIdea} onEdit={onEdit} />)
fireEvent.click(getByText('Edit'))
expect(onEdit).toHaveBeenCalled()
})
})
3. Reusability
// Use the same hooks in different contexts
function IdeaList() {
const { ideas, updateIdea } = useIdeaManager()
return <div>{/* List view instead of canvas */}</div>
}
function IdeaKanban() {
const { ideas, updateIdea } = useIdeaManager()
return <div>{/* Kanban board view */}</div>
}
4. Maintainability
- Each file has a single responsibility
- Easy to locate and fix bugs
- Changes to canvas don't affect data management
- New features can be added without touching existing code
5. Type Safety
- Shared types between hooks and components
- Auto-completion in IDE
- Compile-time error checking
Migration Strategy
Phase 1: Extract Hooks (Don't break existing code)
- Create new hook files
- Copy logic from component to hooks
- Test hooks independently
- Keep original component working
Phase 2: Refactor Component
- Replace inline logic with hook calls
- Test that behavior is identical
- Remove commented-out old code
Phase 3: Extract UI Components
- Identify reusable UI patterns
- Create small components
- Replace inline JSX with components
Phase 4: Create JSON Schema (Optional)
- Define page schema for dynamic loading
- Test schema with PageRenderer
- Switch to schema-driven approach
Additional Examples
Example: Breaking Down ModelDesigner
Before: 400 LOC monolithic component
After:
use-model-state.ts(50 LOC) - Model data managementuse-field-editor.ts(40 LOC) - Field editing logicuse-relation-builder.ts(45 LOC) - Relation logicModelCard.tsx(60 LOC) - Individual model displayFieldList.tsx(50 LOC) - Field list UIRelationGraph.tsx(80 LOC) - Visual relation graphModelDesigner.tsx(130 LOC) - Main orchestration
Total: 455 LOC across 7 files (vs 400 LOC in 1 file) Benefit: Each piece is testable and reusable
Example: Breaking Down WorkflowDesigner
Before: 600 LOC monolithic component
After:
use-workflow-state.ts(55 LOC)use-node-manager.ts(60 LOC)use-workflow-canvas.ts(75 LOC)WorkflowNode.tsx(80 LOC)NodeConfig.tsx(90 LOC)WorkflowToolbar.tsx(50 LOC)WorkflowDesigner.tsx(140 LOC)
Total: 550 LOC across 7 files Benefit: Canvas logic separated from node configuration
Checklist for Refactoring
- Identify data management logic → Extract to hook
- Identify UI state logic → Extract to hook
- Identify reusable UI patterns → Extract to components
- Ensure all pieces are under 150 LOC
- Write tests for hooks
- Write tests for components
- Document hook APIs
- Create JSON schema if applicable
- Update imports in dependent files
- Remove old code
Next Steps
- Start with the largest components first
- Use this pattern as a template
- Create additional hooks as needed
- Build a library of reusable UI components
- Define common pages as JSON schemas
- Document all new hooks and components