← Back to Blog

Refactoring a 9,000-Line React Component: When NOT to Split

Conventional wisdom says large components are bad. Split them up. Single responsibility principle. Keep files under 200 lines. We tried that with App.tsx—and failed. Here's why we kept it at 9,313 lines and what we did instead.

The Component That Couldn't Be Split

EF-Map's App.tsx is the coordinator for our Three.js-based star map. It manages:

The problem with splitting: circular dependencies. The animation loop needs refs from scene setup. Scene setup needs callbacks that update state. State updates trigger re-renders that must not recreate the Three.js scene. Everything is interconnected.

The Failed Experiment

We attempted to extract a SceneManager class (December 2024). After 3 days: orphaned code fragments, broken brace matching, and ref timing bugs that only appeared in production. We reverted and documented the failure in docs/working_memory/2025-11-21_threejs-scene-extraction.md.

The Real Solution: Custom Hooks

Instead of splitting the file, we extracted self-contained logic into custom hooks. The key insight: hooks that don't need direct scene refs can live outside App.tsx.

Hooks We Extracted (15+)

Hook Lines Purpose
useRoutingWorker ~200 Web Worker communication for pathfinding
useCameraAnimation ~150 Smooth camera transitions with easing
useReachability ~180 Jump range calculations and visualization
useHelperBridge ~120 Native overlay helper protocol
useSmartAssemblyManagement ~250 Smart Gate and assembly data loading
useSolarSystemView ~180 Detailed system view with celestials
useKeyboardShortcuts ~100 Global keybindings (Escape, R, etc.)

The Extraction Criteria

A piece of logic is a good extraction candidate when:

  1. It doesn't need scene refs: No direct access to rendererRef, cameraRef, sceneRef
  2. State is self-contained: The hook manages its own useState/useRef without reading App's refs
  3. Communication is via callbacks: The hook exposes functions that App calls, rather than reaching into App's internals
// ✅ GOOD: Self-contained hook
function useRoutingWorker() {
  const [route, setRoute] = useState(null);
  const workerRef = useRef(null);
  
  const calculate = useCallback(async (params) => {
    // Worker communication is isolated
    const result = await workerRef.current.postMessage(params);
    setRoute(result);
  }, []);
  
  return { route, calculate };
}

// ❌ BAD: Would require scene refs
function useStarHover() {
  // This needs raycasterRef, cameraRef, sceneRef...
  // Keep it in App.tsx
}

What Stays in App.tsx

Some things must stay in the coordinator component:

The Section Map Approach

Since App.tsx will remain large, we documented its structure in eve-frontend-map/docs/APP_TSX_ARCHITECTURE.md:

// App.tsx Section Map (~9,300 lines)
// Lines 1-200:     Imports & type definitions
// Lines 200-500:   State declarations (50+ refs)
// Lines 500-800:   Custom hook integrations
// Lines 800-1400:  Scene initialization useEffect
// Lines 1400-2000: Event handlers (click, hover, keyboard)
// Lines 2000-2600: Animation loop (requestAnimationFrame)
// Lines 2600-3200: Camera controls & transitions
// Lines 3200-4000: Routing integration
// Lines 4000-5300: Panel coordination
// Lines 5300-6000: Cinematic mode
// Lines 6000-9300: JSX render & cleanup

New contributors can jump to the relevant section without understanding the entire file.

Metrics That Matter

Metric Before Hooks After Hooks
App.tsx lines ~12,000 ~9,300
Extracted hooks 0 15+
Testable units 1 (integration only) 15+ (unit testable)
Bug surface area Entire file Isolated to hook

When IS It Right to Split?

This approach isn't universal. Split your component when:

Our UI panels (RoutingPanel.tsx, SearchPanel.tsx, etc.) are correctly split—they receive props and emit events, no shared refs needed.

Lessons for Large Codebases

  1. Document decisions: We recorded the failed extraction attempt. Future developers won't repeat it.
  2. Extract what you can: Even partial extraction (hooks) improves testability.
  3. Section maps beat refactoring: For truly interconnected code, navigation docs are more valuable than forced splits.
  4. Measure actual pain: A 9,000-line file that works is better than a fractured architecture with subtle bugs.
"The best refactoring is the one that reduces bugs without introducing new ones. Sometimes that means not refactoring at all."

Related Posts

react refactoringcustom hooksapp.tsxthree.jscode architecturetechnical debtcoordinator componentlarge components