Co-Study4Grid / docs /architecture /app-refactoring-plan.md
github-actions[bot]
Deploy 7688ef1
13d4e44
|
Raw
History Blame Contribute Delete
12.7 kB

App.tsx Refactoring Plan: Extract Custom Hooks

Status: SHIPPED (historical plan). Both refactor waves have landed and this document is preserved as the commit-trail log, not a live plan. App.tsx is now ~1400 lines (down from 2100; the post-0.7.0 hooks useOverflowIframe / useDiagramHighlights and the design-token migration nudged it back up from the 1150 floor reached after PR #109). It is the state-orchestration hub described in frontend/CLAUDE.md. For the current architecture, start with frontend/CLAUDE.md; for the latest extraction round (Phase 2 hook extraction — useN1Fetch, useDiagramHighlights, AppSidebar, SidebarSummary, StatusToasts) see the App.tsx refactor-history table at the bottom of that file and the 0.6.5 / 0.7.0 entries in CHANGELOG.md.

Completed waves:

  1. Phase 1 (PR #74, committed) — extract the five pre-written hooks listed below, plus presentational components.
  2. Phase 2 hook extraction (PR #109, commit 5869ad1) — split the N-1 fetch pipeline into useN1Fetch, the per-tab SVG highlight pipeline into useDiagramHighlights, and the sidebar layout into AppSidebar / SidebarSummary / StatusToasts. Reduced App.tsx from 1575 to ~1150 lines.

Deferred (explicitly scoped out of PR #109 — see the refactor history table in frontend/CLAUDE.md): orchestrator hooks (useSettingsOrchestration, useSaveLoadSession, useStateReset), AppContext-provider conversion, and the NDJSON parser extraction for handleSimulateUnsimulatedAction.

Goal (as of Phase 1)

Refactor the 2100-line App.tsx into a lean orchestrator by wiring up 5 pre-written custom hooks that extract state and logic.

Initial State (as of Phase 1 start)

  • 5 hook files exist in frontend/src/hooks/ (untracked):
    • useSettings.ts (253 lines) - paths, recommender params, settings modal
    • useActions.ts (121 lines) - action selection/rejection/favorite state
    • useAnalysis.ts (219 lines) - result, overloads, run-analysis flow
    • useDiagrams.ts (660 lines) - diagrams, pan/zoom, SLD, voltage filter, zoom-to-element
    • useSession.ts (338 lines) - save/restore session
  • usePanZoom.ts (288 lines) - already committed and used by both App.tsx and useDiagrams
  • Type imports in all 5 hooks have been fixed (React.Dispatch -> Dispatch, etc.)
  • useSettings has been partially integrated into App.tsx (Step 2 done)
  • App.tsx then compiled clean at ~1959 lines.

Hook Interfaces Summary

useSettings() -> SettingsState

Provides: networkPath, actionPath, layoutPath, outputFolderPath + setters; all recommender params + setters; monitoring settings; actionDict info; settings modal state; pickSettingsPath, handleOpenSettings, handleCloseSettings, buildConfigRequest, applyConfigResponse, createCurrentBackup

useActions() -> ActionsState

Provides: selectedActionIds, manuallyAddedIds, rejectedActionIds, suggestedByRecommenderIds + setters; handleActionFavorite(actionId, setResult), handleActionReject(actionId), handleManualActionAdded(actionId, detail, linesOverloaded, setResult, onSelectAction), clearActionState()

useAnalysis() -> AnalysisState

Provides: result/setResult, pendingAnalysisResult, analysisLoading, infoMessage, error + setters; selectedOverloads, monitorDeselected; prevResultRef; handleRunAnalysis(selectedBranch, clearContingencyState, setSuggestedByRecommenderIds), handleDisplayPrioritizedActions(selectedActionIds), handleToggleOverload(overload)

Note: handleRunAnalysis does NOT call setActiveTab('overflow') on PDF event (the original App.tsx does). App.tsx must add an effect to handle this.

useDiagrams(branches, voltageLevels) -> DiagramsState

Provides: activeTab, diagrams (n/n1/action), selectedActionId, actionViewMode, viewBox, inspectQuery, SVG container refs, pan/zoom instances, metadata indices, voltage filter state, vlOverlay, branch refs (committedBranchRef, restoringSessionRef, lastZoomState, actionSyncSourceRef); fetchBaseDiagram, handleActionSelect(actionId, result, selectedBranch, vlLength, setResult, setError), zoom handlers, SLD handlers, handleAssetClick(actionId, assetName, tab, selectedActionId, handleActionSelectFn), zoomToElement, inspectableItems, selectedBranchForSld ref

useSession() -> SessionState

Provides: showReloadModal, sessionList, sessionListLoading, sessionRestoring; handleSaveResults(SaveParams), handleOpenReloadModal(outputFolderPath, setError), handleRestoreSession(sessionName, RestoreContext)

Decomposed Steps

Step 1: Add hook imports (DONE)

Added import lines for all 5 hooks at top of App.tsx.

Step 2: Integrate useSettings (DONE)

  • Instantiated useSettings() and destructured all properties
  • Removed old useState declarations for settings
  • Removed old handleOpenSettings, handleCloseSettings, pickSettingsPath
  • Removed localStorage load/persist effects
  • Updated handleApplySettings and handleLoadConfig to use buildConfigRequest() and applyConfigResponse()

Step 3: Integrate useActions

Remove from App.tsx:

  • useState for: selectedActionIds, manuallyAddedIds, rejectedActionIds, suggestedByRecommenderIds
  • handleActionFavorite, handleActionReject callbacks

Add:

  • const actionsHook = useActions(); and destructure
  • Wrapper for handleManualActionAdded that passes setResult and wrappedActionSelect
  • Wrapper for handleActionFavorite that passes setResult

Step 4: Integrate useAnalysis

Remove from App.tsx:

  • useState for: result, pendingAnalysisResult, analysisLoading, infoMessage, error, selectedOverloads, monitorDeselected
  • prevResultRef
  • infoMessage auto-clear effect
  • handleRunAnalysis, handleDisplayPrioritizedActions, handleToggleOverload callbacks

Add:

  • const analysis = useAnalysis(); and destructure
  • Wrapper for handleRunAnalysis that passes selectedBranch, clearContingencyState, setSuggestedByRecommenderIds
  • Wrapper for handleDisplayPrioritizedActions that passes selectedActionIds
  • Effect: when result?.pdf_url changes during analysisLoading, call setActiveTab('overflow')

Update:

  • clearContingencyState to call analysis setters + actionsHook.clearActionState()
  • hasAnalysisState to reference hook state
  • Remove error/setError local state (now from analysis hook)

Step 5: Integrate useDiagrams

Remove from App.tsx:

  • useState for: activeTab, nDiagram, n1Diagram, n1Loading, selectedActionId, actionDiagram, actionDiagramLoading, actionViewMode, originalViewBox, inspectQuery, nominalVoltageMap, uniqueVoltages, voltageRange, vlOverlay
  • useRef for: nSvgContainerRef, n1SvgContainerRef, actionSvgContainerRef, committedBranchRef, restoringSessionRef, lastZoomState, actionSyncSourceRef, activeTabRef, prevTabRef
  • usePanZoom calls (nPZ, n1PZ, actionPZ)
  • useMemo for metadata indices (nMetaIndex, n1MetaIndex, actionMetaIndex)
  • useMemo for inspectableItems
  • fetchBaseDiagram, handleActionSelect, handleViewModeChange, handleManualZoomIn/Out, handleManualReset
  • handleVlDoubleClick, handleOverlaySldTabChange, handleOverlayClose, handleAssetClick
  • fetchSldVariant, zoomToElement
  • Tab synchronization useLayoutEffect
  • Action sync useEffect
  • id-map cache invalidation effects
  • Voltage range filter logic + effects

Add:

  • const diagrams = useDiagrams(branches, voltageLevels); and destructure
  • Effect: sync diagrams.selectedBranchForSld.current = selectedBranch
  • Wrapper for handleActionSelect that passes result, selectedBranch, voltageLevels.length, setResult, setError
  • Wrapper for handleAssetClick that passes diagrams.selectedActionId, wrappedActionSelect

Keep in App.tsx (NOT in useDiagrams):

  • N-1 diagram fetch effect (uses selectedBranch, branches, hasAnalysisState, clearContingencyState)
  • Highlights effect (applyHighlightsForTab + its useEffect)
  • Auto-zoom effect (inspectQuery/selectedBranch -> zoomToElement)
  • Overloads sync effect (n1Diagram -> setSelectedOverloads)

Step 6: Integrate useSession

Remove from App.tsx:

  • useState for: showReloadModal, sessionList, sessionListLoading, sessionRestoring
  • handleSaveResults, handleOpenReloadModal, handleRestoreSession callbacks

Add:

  • const session = useSession();
  • Wrapper for handleSaveResults that builds SaveParams from settings/analysis/actions/diagrams state
  • Wrapper for handleOpenReloadModal that passes outputFolderPath, setError
  • Wrapper for handleRestoreSession that builds RestoreContext from all hooks

Step 7: Clean up imports

Remove unused imports from App.tsx:

  • usePanZoom (now used inside useDiagrams)
  • buildMetadataIndex, getIdMap, invalidateIdMapCache (now in useDiagrams)
  • buildSessionResult (now in useSession)
  • Unused type imports: DiagramData, ViewBox, MetadataIndex, SettingsBackup, VlOverlay, SldTab, FlowDelta, AssetDelta, SessionResult, CombinedAction

Keep:

  • applyOverloadedHighlights, applyDeltaVisuals, applyActionTargetHighlights, applyContingencyHighlight (used in highlights effect)
  • processSvgAsync (used in N-1 fetch effect)
  • api (used in handleLoadConfig, handleApplySettings, N-1 fetch)
  • Type imports: ActionDetail, TabId (used in highlights effect)

Step 8: Verify & commit

  • Run npx tsc --noEmit to verify compilation
  • Run npm run lint to check ESLint
  • Fix any issues
  • Commit and push

Cross-Hook Wiring (App.tsx orchestration)

These wrapper callbacks in App.tsx connect the hooks together:

// Wrap diagrams.handleActionSelect with current state
const wrappedActionSelect = (actionId: string | null) =>
  diagrams.handleActionSelect(actionId, result, selectedBranch, voltageLevels.length, setResult, setError);

// Wrap actionsHook.handleActionFavorite with setResult
const wrappedActionFavorite = (actionId: string) =>
  actionsHook.handleActionFavorite(actionId, setResult);

// Wrap actionsHook.handleManualActionAdded with setResult + action select
const wrappedManualActionAdded = (actionId: string, detail: ActionDetail, linesOverloaded: string[]) =>
  actionsHook.handleManualActionAdded(actionId, detail, linesOverloaded, setResult, wrappedActionSelect);

// Wrap analysis.handleRunAnalysis with current branch + clearContingencyState
const wrappedRunAnalysis = () =>
  analysis.handleRunAnalysis(selectedBranch, clearContingencyState, setSuggestedByRecommenderIds);

// Wrap analysis.handleDisplayPrioritizedActions with selectedActionIds
const wrappedDisplayPrioritized = () =>
  analysis.handleDisplayPrioritizedActions(selectedActionIds);

// Wrap diagrams.handleAssetClick with current selectedActionId + action select
const wrappedAssetClick = (actionId: string, assetName: string, tab: 'action' | 'n' | 'n-1' = 'action') =>
  diagrams.handleAssetClick(actionId, assetName, tab, diagrams.selectedActionId, wrappedActionSelect);

App-Level Effects (remain in App.tsx)

  1. N-1 fetch effect - watches selectedBranch, handles confirmation dialog, calls api.getN1Diagram
  2. Overloads sync - watches n1Diagram?.lines_overloaded -> setSelectedOverloads
  3. PDF tab switch - watches result?.pdf_url + analysisLoading -> setActiveTab('overflow')
  4. Highlights effect - applyHighlightsForTab for n-1/action tabs
  5. Auto-zoom effect - watches inspectQuery/selectedBranch -> zoomToElement
  6. SLD branch sync - watches selectedBranch -> diagrams.selectedBranchForSld.current

App-Level State (remains in App.tsx)

  • branches, setBranches - string[]
  • voltageLevels, setVoltageLevels - string[]
  • selectedBranch, setSelectedBranch - string
  • configLoading, setConfigLoading - boolean
  • confirmDialog, setConfirmDialog - confirmation dialog state

JSX Changes

The JSX stays structurally identical. Variable references change:

  • networkPath -> still networkPath (destructured from settings)
  • handleActionSelect -> wrappedActionSelect
  • handleActionFavorite -> wrappedActionFavorite
  • handleManualActionAdded -> wrappedManualActionAdded
  • handleRunAnalysis -> wrappedRunAnalysis
  • handleDisplayPrioritizedActions -> wrappedDisplayPrioritized
  • handleAssetClick -> wrappedAssetClick
  • handleSaveResults -> wrappedSaveResults
  • handleOpenReloadModal -> wrappedOpenReloadModal
  • handleRestoreSession -> wrappedRestoreSession
  • sessionRestoring -> session.sessionRestoring
  • showReloadModal -> session.showReloadModal
  • etc.