Skip to main content

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 under equa-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-dom 5.1.2) with redux-first-history (replaced connected-react-router in PR #465)
  • Route groups: profileRoutes, guestRoutes, guestOrganizationsRoutes, organizationListRoutes, organizationRoutes, routes
  • Protection: Routes flagged as protected redirect 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 via React.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.
What stays in the main bundle (static imports):
  • 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)
What is lazy-loaded (137 dynamic imports across 25+ module groups):
  • 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
The production build produces 48+ JS chunks: 4 named entrypoint chunks (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:
  1. Always use lazyPage() for the component import — never add a static import from a module barrel
  2. Follow the pattern: const NewPage = lazyPage(() => import('@modules/new-module').then(m => ({ default: m.NewPage })))
  3. The only exception is @modules/auth/pages which 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, axios 0.21.1 for multipart/form-data
  • Base URL: Configured via API_URL (default: /api/v1)
  • Credentials: credentials: 'include' (session cookies)
  • Error handling: HttpError class 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:
This lets the normal redux-loop failure path dispatch, the 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 in equa-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

The babel-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 uses cacheGroups 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 imports core-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:
Never use 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 in routes.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.
Output: ~/.claude/comet-browser/output/audit/s050-perf/perf-report.json