16 KiB
Refactoring App.tsx Design Document
Problem Description
The main application component, App, has grown into a large and complex monolith that coordinates URL routing, query parameter synchronization, asynchronous data fetching, UI layout rendering, and external integrations like the WebMCP bridge. Because these distinct responsibilities are combined within a single file, the codebase has become difficult to navigate, test, and safely modify without introducing unintended regressions. This refactoring effort aims to systematically decouple these concerns by decomposing the monolith into small, single-responsibility modules, separating the page-level UI layouts, and establishing the URL query parameters as the single source of truth for viewer state.
The Technical Plan
At a high level, the refactored architecture splits user interface representation from business logic and external connections. Instead of a single central orchestrator holding all states, the app relies on the browser's URL to drive the active view and delegates sub-features (like data fetching, authentication, and external syncing) to dedicated hooks and services.
Explanatory Overview
- Routing & Page Views: The root App component will serve purely as a routing switch, but will retain the Google Drive "Open with" flow redirection logic. It decides whether to display the landing screen (
IntroPage) or the visual family tree explorer (ViewPage). It is also responsible for stripping external authorization query parameters (likestateandauthcode) from the browser window's URL. - Layout Nesting (TopBar & SidePanel): Rather than having a global layout shell wrapping all pages, each page will compose its own layout explicitly. This allows the
ViewPageto pass live chart selection data, configuration, and export triggers directly to its own renderedTopBar, while theIntroPageremains clean and completely decoupled from viewer states. To prevent locking the user interface on loading errors,ViewPagewill render theTopBareven when in loading or error states. - URL as Single Source of Truth (SSOT): Rather than keeping user settings (like the selected person, active chart view, and display preferences) in temporary React memory and continuously syncing them to the URL, the application will read these properties directly from the URL query string on every render. This includes the detail selection sidebar state (
detailIndiwill map to?detail=...). Changing a setting now simply updates the browser's URL, and React automatically updates the view. - Data Service Layer: All logic involved in downloading, caching, and parsing GEDCOM or WikiTree files from different sources is fully isolated from the user interface, triggered via a unified API spec on the
ViewPage. - WebMCP & Auth Hooks: Third-party integrations and security layers (like the WebMCP developer bridge and Google Drive sign-in states) are extracted into custom React hooks. These hooks manage side effects independently, removing clutter from the layout rendering code.
Alternatives Considered
1. Global State / Prop Drilling in Root Component
- Approach: Keep all the viewer and configuration state inside the root
Appcomponent and pass them down as props to the nested components (such asIntroPageandViewPage). - Why Rejected: This would fail to address the core problem. The root
app.tsxfile would remain a massive state-managing monolith containing dozens of hooks and effects, making it just as hard to maintain and test as before.
2. React Context for Viewer Session
- Approach: Create a global React Context (e.g.,
GenealogySessionContext) that holds the active viewer's data, parameters, and callback handlers, and wrap the application root in a Context Provider. - Why Rejected: While a valid pattern, introducing Context adds boilerplate and can lead to unnecessary re-renders. Explicit page-level nesting—where
ViewPageandIntroPageeach explicitly render their ownTopBarheaders—is a simpler and more isolated design. It allows components to consume direct, local props without global state providers.
3. React-State-to-URL Syncing (Bidirectional Sync)
- Approach: Maintain React state variables (
useState) for the selected individual, chart view type, and configuration, and keep them synchronized with the URL query parameters usinguseEffectloops. - Why Rejected: Bidirectional synchronization is extremely error-prone and a frequent cause of infinite rendering loops and race conditions (especially with async network requests). Transitioning to the URL as the Single Source of Truth (deriving all active states directly from the URL on render) completely removes the sync hooks and ensures browser back/forward buttons work correctly out of the box.
4. Component-First Refactoring (Extracting Visual Leaf Nodes)
- Approach: Start by extracting pure UI layout leaf nodes (such as the chart display wrappers or sidebar layouts) before addressing the business logic.
- Why Rejected: The primary complexity in
app.tsxlies in the lifecycle management and side effects, not the JSX markup size. Starting with leaf components leaves the complex state orchestrations untouched. Instead, we prioritized extracting utility modules, isolated background hooks (likeWebMcpBridge), and transitioning states to the URL before separating UI pages.
Detailed Implementation
To refactor app.tsx safely and maintain continuous code correctness, we will modify one file and create eight new ones. This section maps out the role of each file and the step-by-step implementation plan.
Affected Files and Rationale
1. [MODIFY] src/app.tsx
- Role: Will be stripped down to a lightweight Router component that only defines page-level paths, coordinates Google Drive "Open with" redirection synchronously on render, and renders
<IntroPage />or<ViewPage />. - Rationale: Eliminates the monolithic coordinator by delegating view orchestration, state synchronization, and page rendering to dedicated page components and hooks, while retaining initial payload interception.
2. [NEW] src/components/error_display.tsx
- Role: Will contain the presentational components
ErrorMessageandErrorPopup. - Rationale: Keeps simple UI leaf components separated from the main business logic and state coordinates.
3. [NEW] src/datasource/instances.ts
- Role: Declares and exports global datasource class instances (
uploadedDataSource,gedcomUrlDataSource,embeddedDataSource,googleDriveDataSource). - Rationale: Avoids declaring stateful data service instances inside layout and view components, improving separation of concerns.
4. [NEW] src/util/url_args.ts
- Role: Exports
getStaticUrl,getParamFromSearch, andgetArgumentsutility functions, along with their related TypeScript interfaces (Arguments,DataSourceSpec), including support for thedetailquery parameter. It also provides a pure helper function (getUrlForArgs) that accepts current/new arguments and returns a path/query object suitable for passing to React Router'snavigatefunction. - Rationale: Moves parsing and decoding logic for query strings out of the React components, allowing this code to be tested independently in isolation.
5. [NEW] src/util/url_args.spec.ts
- Role: Jest unit tests verifying the correctness of
getArgumentsunder different query configurations (e.g. WikiTree authcodes, Google Drive file IDs, static URLs, local uploads). - Rationale: URL parsing acts as the gateway to all loader code; ensuring this logic is 100% correct via unit testing prevents regressions during URL SSOT changes.
6. [NEW] src/hooks/use_webmcp_bridge.ts
- Role: Custom React hook
useWebMcpBridge(data, detailIndi, onSelection)called inViewPageencapsulating the WebMCP bridge creation, tool registration, state synchronization, and cleanup effects. - Rationale: Isolates external developer tools syncing from the UI components.
7. [NEW] src/hooks/use_google_auth.ts
- Role: Custom React hook
useGoogleAuth()encapsulating access to the globalgoogleDriveServiceauthentication token, session storage cache clearing, and login/logout trigger states. - Rationale: Provides shared authentication state that can be consumed by the
TopBarheaders of both page components without needing parent state lifting.
8. [NEW] src/pages/view_page.tsx
- Role: The page view orchestrating the chart visualizer workspace. It handles asynchronous loader triggers, keeps local data loading states, derives view configurations from the URL, and renders the Sidebar, SidePanel, and its own explicit
TopBar(including rendering on loading/error states). It also hosts the state and rendering for<GoogleAuthModal>to handle Google Drive auth errors caught during data loading, handles memoization ofWikiTreeDataSource(due to its react-intl context requirement), performs Object URL cleanup on unmount, and runsupdateChartWithConfigsynchronously during the render pass to synchronize URL-derived configuration settings with the in-memory mutated chart data. - Rationale: Consolidates the chart workspace state and render tree away from the root router component.
9. [NEW] src/pages/intro_page.tsx
- Role: The landing page component wrapping the
Intropresentation and rendering its own simpleTopBar. - Rationale: Decouples the landing screen structure from the chart visualizer's stateful layout shell.
Step-by-Step Execution Plan
Phase 1: Pure Component and Utility Extraction
- Step 1.1: Create
src/components/error_display.tsxand moveErrorMessageandErrorPopupfromsrc/app.tsx. Update imports insrc/app.tsx. - Step 1.2: Create
src/datasource/instances.tsand move data source class instantiations. Update references insrc/app.tsx. RefactorEmbeddedDataSourceto clean up its message event listener when the loading promise resolves or rejects (or track listener state) to prevent duplicate event listener leaks on multiple page mounts. - Step 1.3: Create
src/util/url_args.ts(the parsing utility) andsrc/util/url_args.spec.ts(its Jest unit test suite) together. Extract URL query parameter parsing functions and types fromsrc/app.tsx, write comprehensive tests, update imports insrc/app.tsx, and runnpm testto verify. - Step 1.4: Modify
src/menu/top_bar.tsxto make chart-specific props and event handlers optional (e.g.data,allowAllRelativesChart,allowPrintAndDownload,eventHandlers, etc.), preparing the component for rendering on the landing screen without dummy properties.
Phase 2: WebMCP Bridge Extraction
- Step 2.1: Create
src/hooks/use_webmcp_bridge.tswrapping WebMCP registration and synchronization effects. - Step 2.2: Replace the inline WebMCP logic and
useEffectblocks insrc/app.tsxwith a single call to the customuseWebMcpBridgehook. Verify usingnpm test. Note: During Phase 4, this hook call will be moved fromsrc/app.tsxintosrc/pages/view_page.tsxwhere the layout state is relocated.
Phase 3: Shifting to URL as Single Source of Truth
We will gradually eliminate React state variables in src/app.tsx and derive them (including settings like standalone, showWikiTreeMenus, and freezeAnimation) directly from the useLocation() search query parameters on render:
- Step 3.1a: Shift
chartTypeto URL as SSOT. Derive it usinguseMemofrom search parameters, replace calls tosetChartTypewith URL updates, and remove the React state variable and sync effect. - Step 3.1b: Shift
standaloneto URL as SSOT. Derive it directly from search parameters on render and remove its React state variable. - Step 3.1c: Shift
showWikiTreeMenusto URL as SSOT. Derive it directly from search parameters on render and remove its React state variable. - Step 3.1d: Shift
freezeAnimationto URL as SSOT. Derive it directly from search parameters on render and remove its React state variable. - Step 3.2: Extract
selectionanddetailIndistate. Parse them directly from URL params (indi,gen, anddetail); update display selectors. Remove React states. Ensure that chart selection changes (onSelectioncallback) explicitly clear or update thedetailquery parameter to match the new selection to avoid getting stuck on the old details viewport. Also update the detail-only selection handler (onDetailSelection) to update thedetailquery parameter in the URL. - Step 3.3: Extract
showSidePanelstate. Derive state directly from?sidePanel=parameter. Remove React state. Enhance URL args helpers to allow generating path/query target objects with replaced values, and ensure layout settings (sidePanel) and configuration changes usereplace: trueto prevent polluting the browser history stack. - Step 3.4: Extract
configstate. Parse display settings on render usingargsToConfighelper. Remove state. InViewPage, runupdateChartWithConfig(config, data)synchronously during the render pass (e.g. in theuseMemothat derives query parameters/config) to update the in-memory chart data before it is rendered by the<Chart>child component. Run E2E and visual tests to verify no rendering regressions occurred.
Phase 4: Structural Page Nesting
- Step 4.1: Create
src/hooks/use_google_auth.tsto manage Google Drive authentication token updates. RemovehasGoogleTokenstate fromsrc/app.tsx. - Step 4.2: Create
src/pages/view_page.tsxcontaining layout rendering (renderMainArea), asynchronous data loaders, view state management,revokeObjectUrlscleanups, and<GoogleAuthModal>error fallback state and rendering. - Step 4.3: Create
src/pages/intro_page.tsxrendering the landing screen and its own local header. - Step 4.4: Refactor
src/app.tsxto serve as a pure routing switch routing to<IntroPage />or<ViewPage />. InApp, handle Google Drive redirection synchronously during the render pass by returning a<Navigate replace />element (preventing render flashing), and ensure the redirection parser checks both the routerlocation.searchand the externalwindow.location.searchto handle HashRouter query structure robustly. Once external parameters (likestateorauthcode) are detected, they must be stripped fromwindow.location.searchusingwindow.history.replaceStateto prevent redirection loops on page refresh. Remove all layout state logic from the root file, including moving theuseWebMcpBridgehook invocation into<ViewPage />. Run final verification scripts (npm run check:all).
Future Considerations
1. Image Object URL Memory Management
- Instead of managing image revocation inside the component lifecycle (which can break back-navigation since the
ViewPageunmounts and revokes URLs), we will explore having the global datasource singletons ininstances.ts(such asUploadedDataSourceandGoogleDriveDataSource) manage the lifetime of their image URL maps. WhenloadDatais called on a datasource, it will check if it has a reference to a previous image map and explicitly revoke it before loading the new dataset.
2. TopBar Layout Refactoring
- Refactor
top_bar.tsxto make all chart-specific event handlers and properties optional, or split the header layout from the stateful workspace menus. This will eliminate passing dummy or empty handlers from theIntroPage.