Frontend Architecture
Repository: equa-web | Stack: React 18.2, TypeScript 5.3, Redux 4, Webpack 4
Last updated: 2026-04-11 (Phase 1 React 18 upgrade, ecosystem libraries, bundle optimization, API client network-error handling, dashboard empty state, gateway token sync)
Entry Point
Source:equa-web/src/index.tsx and equa-web/lab/main-prod.ts
The app initializes polyfills (core-js/stable + regenerator-runtime/runtime), creates a Redux store via newStore(), creates browser history with createBrowserHistory(), wraps in a Redux Provider, and renders to #root via createRoot() from react-dom/client (upgraded from ReactDOM.render() in PR #506).
Module Inventory
The frontend is organized into feature modules underequa-web/src/modules/:
Routing and Code Splitting
Source:equa-web/src/app/routes/routes.ts, equa-web/src/app/routes/lazy.tsx, equa-web/src/app/components/routes/routes.tsx
- Router: React Router v5 (
react-router-dom5.1.2) withredux-first-history(replacedconnected-react-routerin PR #465) - Route groups:
profileRoutes,guestRoutes,guestOrganizationsRoutes,organizationListRoutes,organizationRoutes,routes - Protection: Routes flagged as
protectedredirect unauthenticated users - HOCs:
withNavigation,withTracker,withService,withHeader,withOrganizationHeader,withProfileHeader - Path constants:
equa-web/src/logic/paths.ts
Lazy Loading (Spec 050)
All feature module page components are lazy-loaded viaReact.lazy() + <Suspense>. The lazyPage() utility in src/app/routes/lazy.tsx wraps each dynamic import with a <Suspense> fallback using the existing <Loading /> spinner component.
- Auth module (
@modules/auth/pages) — needed for login page to render immediately - Shell components: headers, navigation, loading, error boundary, permissions HOCs
- Redux reducers (all 10 registered at store initialization per clarification C5)
- All other page components: captable, ESOP, organization, convertibles, payments, profile, guest, admin, referrals, reports, roles, documents, welcome, team-members, hh-finance, google-drive, equabot-settings, agreements, landing, marketing, messaging, user-dashboard, organization-dashboard
react-vendor, styled, vendor, main) plus 4 async vendor chunks (crypto-vendor, pixi-vendor, form-vendor, pdf-vendor) plus ~40 lazy module chunks.
Chunk Error Boundary
Source:equa-web/src/app/components/routes/chunk-error-boundary.tsx
A ChunkErrorBoundary wraps the route <Switch> in routes.tsx. If a lazy chunk fails to load (network error, deployment mismatch), it catches the error via getDerivedStateFromError and renders a “Failed to load this page” message with a Retry button instead of a white screen.
Adding New Routes
When adding a new route or module page:- Always use
lazyPage()for the component import — never add a staticimportfrom a module barrel - Follow the pattern:
const NewPage = lazyPage(() => import('@modules/new-module').then(m => ({ default: m.NewPage }))) - The only exception is
@modules/auth/pageswhich must stay static for the login critical path
State Management
Source:equa-web/src/logic/store.ts, equa-web/src/logic/reducers/root-reducer.ts
Reducers
API Communication
Source:equa-web/src/service/lib/http-client.ts, equa-web/src/service/services/web-client.ts
- HTTP client: Native
fetch()for standard requests,axios0.21.1 for multipart/form-data - Base URL: Configured via
API_URL(default:/api/v1) - Credentials:
credentials: 'include'(session cookies) - Error handling:
HttpErrorclass with auto-logout on 401 - Methods:
get(),post(),patch(),put(),delete(),postMultipart(),postFiles()
Network Error Handling (PR #514)
baseRequest() in http-client.ts wraps every fetch() call in a try-catch. When the backend is unreachable — DNS failure, CORS block, connection refused, offline — fetch() throws a TypeError. Rather than letting that throw propagate as an unhandled rejection (which was the root cause of the infinite splash screen bug, issue #482), the client converts it into a structured error:
getCurrentUser callback fires, initialLoad flips to true, and the app renders the login page instead of hanging on the Equa logo spinner forever.
Callers do not need special handling — the network-error response follows the same HttpError contract as 4xx/5xx responses. Anywhere the code already checks isHttpError(result), the network-error case is handled automatically.
Source: equa-web/src/service/lib/http-client.ts (PR #514, 2026-04-02).
Service modules
Located inequa-web/src/service/services/:
actions,billing,captable,google-drive,organizations,payments,profile,roles,wallet
Component Library
Source:equa-web/package.json (dependency: equa-patternlib from GitHub)
The pattern library provides 26+ shared UI components:
Avatar, Badge, Button, Card, Checkbox, Chip, DatePicker, Dropdown, FileUpload, Input, Menu, Modal, Pagination, Progress, Radio, Rating, Sidebar, Skeleton, Slider, Stepper, Switch, Table, Tabs, Toast, Toggle, Tooltip
Components are re-exported via equa-web/src/shared/components/.
Styling
Theme switching is supported and persisted via cookies.
Font Loading
Six NunitoSans variants are declared via@font-face in src/styles/global.ts. All declarations include font-display: swap to prevent Flash of Invisible Text (FOIT). When adding new @font-face rules, always include font-display: swap.
styled-components Build Plugin
Thebabel-plugin-styled-components (^2.1.4) is active in the webpack babel-loader config. In production builds it strips displayName and enables pure annotation for dead code elimination. In development it preserves displayName for debugging.
Webpack Configuration
Source:equa-web/webpack.config.js
splitChunks (Production Only)
The production build usescacheGroups to separate vendor code into named chunks for optimal caching:
Initial chunks (loaded on every page):
Async chunks (loaded on-demand when navigating to specific routes):
Additional settings:
maxInitialRequests: 10, minSize: 20000. The async vendor chunks were added in PR #510 to reduce the initial page load by ~2.5 MB — pixi.js, crypto libs, and the login background image (compressed 95% from 1.93 MB to 93 KB) are no longer loaded on first visit.
Performance Budgets
These budgets emit build warnings for oversized chunks. The entrypoint size was significantly reduced in PR #510 by moving heavy vendor libraries to async chunks (~2.5 MB reduction). Further reduction requires upgrading to Webpack 5 (better tree shaking) or migrating to Vite (Phase 2).
Polyfill Strategy
The entry point importscore-js/stable and regenerator-runtime/runtime (replacing the deprecated @babel/polyfill). The @babel/preset-env handles transpilation targeting browsers specified in the webpack config.
Path Aliases
Key Utilities
Coding Conventions
Lodash Imports
Use per-function imports, never the full library:import _ from 'lodash' — this pulls the entire 72 KB library into the bundle even if only one function is used. If a file doesn’t actually call any lodash function, remove the import entirely.
Route Imports
All page component imports inroutes.ts must use lazyPage() dynamic imports. The only exception is @modules/auth/pages (static for the login critical path). See Routing and Code Splitting above.
Dependency Hygiene
Before adding a new dependency, check if it will end up in the initial entrypoint bundle or a lazy chunk. Heavy libraries (>100 KB) should only be imported from lazy-loaded modules. The following unused packages were removed during Spec 050 and must not be re-added:moment (use date-fns instead), recharts, react-hot-loader, @hot-loader/react-dom.
Testing
Performance Testing
Source:Comet-Bridge/scripts/perf-audit.mjs
An automated performance audit script runs via Comet-Bridge Playwright against the equa-web dev server or production build. It navigates all major route groups, captures performance.timing metrics (TTI, DCL, load time), checks for chunk load errors, and produces a JSON report with screenshots.
~/.claude/comet-browser/output/audit/s050-perf/perf-report.json