mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-07-17 17:21:48 +00:00
304 lines
16 KiB
Markdown
304 lines
16 KiB
Markdown
# Refactoring App.tsx Design Document
|
|
|
|
## Problem Description
|
|
|
|
The main application component, [App](../src/app.tsx), 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](../src/app.tsx) 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 (like
|
|
`state` and `authcode`) 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 `ViewPage` to pass live chart selection data, configuration,
|
|
and export triggers directly to its own rendered `TopBar`, while the
|
|
`IntroPage` remains clean and completely decoupled from viewer states. To
|
|
prevent locking the user interface on loading errors, `ViewPage` will render
|
|
the `TopBar` even 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 (`detailIndi`
|
|
will 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
|
|
`App` component and pass them down as props to the nested components (such as
|
|
`IntroPage` and `ViewPage`).
|
|
- **Why Rejected:** This would fail to address the core problem. The root
|
|
`app.tsx` file 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
|
|
`ViewPage` and `IntroPage` each explicitly render their own `TopBar`
|
|
headers—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 using `useEffect` loops.
|
|
- **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.tsx` lies 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 (like
|
|
`WebMcpBridge`), 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 `ErrorMessage` and
|
|
`ErrorPopup`.
|
|
- **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`, and `getArguments`
|
|
utility functions, along with their related TypeScript interfaces
|
|
(`Arguments`, `DataSourceSpec`), including support for the `detail` query
|
|
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's `navigate` function.
|
|
- **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 `getArguments` under
|
|
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 in `ViewPage` encapsulating 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
|
|
global `googleDriveService` authentication token, session storage cache
|
|
clearing, and login/logout trigger states.
|
|
- **Rationale:** Provides shared authentication state that can be consumed by
|
|
the `TopBar` headers 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 of
|
|
`WikiTreeDataSource` (due to its react-intl context requirement), performs
|
|
Object URL cleanup on unmount, and runs `updateChartWithConfig` synchronously
|
|
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 `Intro` presentation and
|
|
rendering its own simple `TopBar`.
|
|
- **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
|
|
|
|
- [x] **Step 1.1:** Create `src/components/error_display.tsx` and move
|
|
`ErrorMessage` and `ErrorPopup` from `src/app.tsx`. Update imports in
|
|
`src/app.tsx`.
|
|
- [x] **Step 1.2:** Create `src/datasource/instances.ts` and move data source
|
|
class instantiations. Update references in `src/app.tsx`. Refactor
|
|
`EmbeddedDataSource` to 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.
|
|
- [x] **Step 1.3:** Create `src/util/url_args.ts` (the parsing utility) and
|
|
`src/util/url_args.spec.ts` (its Jest unit test suite) together. Extract
|
|
URL query parameter parsing functions and types from `src/app.tsx`, write
|
|
comprehensive tests, update imports in `src/app.tsx`, and run `npm test`
|
|
to verify.
|
|
- [x] **Step 1.4:** Modify `src/menu/top_bar.tsx` to 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
|
|
|
|
- [x] **Step 2.1:** Create `src/hooks/use_webmcp_bridge.ts` wrapping WebMCP
|
|
registration and synchronization effects.
|
|
- [x] **Step 2.2:** Replace the inline WebMCP logic and `useEffect` blocks in
|
|
`src/app.tsx` with a single call to the custom `useWebMcpBridge` hook.
|
|
Verify using `npm test`. Note: During Phase 4, this hook call will be
|
|
moved from `src/app.tsx` into `src/pages/view_page.tsx` where 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:
|
|
|
|
- [x] **Step 3.1a:** Shift `chartType` to URL as SSOT. Derive it using `useMemo`
|
|
from search parameters, replace calls to `setChartType` with URL updates,
|
|
and remove the React state variable and sync effect.
|
|
- [x] **Step 3.1b:** Shift `standalone` to URL as SSOT. Derive it directly from
|
|
search parameters on render and remove its React state variable.
|
|
- [x] **Step 3.1c:** Shift `showWikiTreeMenus` to URL as SSOT. Derive it
|
|
directly from search parameters on render and remove its React state
|
|
variable.
|
|
- [x] **Step 3.1d:** Shift `freezeAnimation` to URL as SSOT. Derive it directly
|
|
from search parameters on render and remove its React state variable.
|
|
- [x] **Step 3.2:** Extract `selection` and `detailIndi` state. Parse them
|
|
directly from URL params (`indi`, `gen`, and `detail`); update display
|
|
selectors. Remove React states. Ensure that chart selection changes
|
|
(`onSelection` callback) explicitly clear or update the `detail` query
|
|
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 the `detail` query parameter in the URL.
|
|
- [x] **Step 3.3:** Extract `showSidePanel` state. 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 use
|
|
`replace: true` to prevent polluting the browser history stack.
|
|
- [x] **Step 3.4:** Extract `config` state. Parse display settings on render
|
|
using `argsToConfig` helper. Remove state. In `ViewPage`, run
|
|
`updateChartWithConfig(config, data)` synchronously during the render pass
|
|
(e.g. in the `useMemo` that 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
|
|
|
|
- [x] **Step 4.1:** Create `src/hooks/use_google_auth.ts` to manage Google Drive
|
|
authentication token updates. Remove `hasGoogleToken` state from
|
|
`src/app.tsx`.
|
|
- [x] **Step 4.2:** Create `src/pages/view_page.tsx` containing layout rendering
|
|
(`renderMainArea`), asynchronous data loaders, view state management,
|
|
`revokeObjectUrls` cleanups, and `<GoogleAuthModal>` error fallback state
|
|
and rendering.
|
|
- [x] **Step 4.3:** Create `src/pages/intro_page.tsx` rendering the landing
|
|
screen and its own local header.
|
|
- [x] **Step 4.4:** Refactor `src/app.tsx` to serve as a pure routing switch
|
|
routing to `<IntroPage />` or `<ViewPage />`. In `App`, 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 router `location.search` and the
|
|
external `window.location.search` to handle HashRouter query structure
|
|
robustly. Once external parameters (like `state` or `authcode`) are
|
|
detected, they must be stripped from `window.location.search` using
|
|
`window.history.replaceState` to prevent redirection loops on page
|
|
refresh. Remove all layout state logic from the root file, including
|
|
moving the `useWebMcpBridge` hook 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 `ViewPage` unmounts and revokes URLs), we will
|
|
explore having the global datasource singletons in `instances.ts` (such as
|
|
`UploadedDataSource` and `GoogleDriveDataSource`) manage the lifetime of their
|
|
image URL maps. When `loadData` is 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.tsx` to 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 the
|
|
`IntroPage`.
|