Spaces:
Sleeping
Sleeping
| # 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`](../../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`](../../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: | |
| ```typescript | |
| // 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. | |