Skip to content

feat: Migrate embedded frontend from custom MVX framework to React 18#338

Merged
sebavan merged 3 commits intomasterfrom
feat/react
Mar 20, 2026
Merged

feat: Migrate embedded frontend from custom MVX framework to React 18#338
sebavan merged 3 commits intomasterfrom
feat/react

Conversation

@sebavan
Copy link
Member

@sebavan sebavan commented Mar 20, 2026

Summary

Replace the custom MVX rendering framework with React 18 functional components, making the codebase contributor-friendly while preserving identical visual output.

What changed

Architecture

  • Adapter pattern: ReactCaptureMenu and ReactResultView implement the exact same public API as the original classes — spector.ts just swaps an import
  • ExternalStore: Lightweight state bridge using useSyncExternalStore (no repeated root.render() anti-pattern)
  • Context pattern: Adapter passes itself to React tree via CaptureMenuContext / ResultViewContext
  • 32 React components replace 31 MVX components (+ JSONRenderTree recursive renderer)

Frontend files

  • src/embeddedFrontend/react/ — all new React components and adapters
  • src/embeddedFrontend/styles/unchanged SCSS (same classes, same selectors)
  • Deleted: mvx/, captureMenu/, resultView/, ux/ (old MVX code)

Testing

  • 38 Playwright visual regression tests with screenshot baselines
  • Covers: CaptureMenu (10 states), ResultView (13 states), SourceCode editor (5), responsive (4), regression tests (2), smoke (3), fixture generation (1)
  • Deterministic rendering: SwiftShader, disabled transitions, fixed viewport

Bug fixes

  • hide() now actually works (MVX had a truthy-check bug on the state object)
  • Removed dead IE11 navigator.msSaveBlob code path
  • Fixed falsy number rendering in VisualStateListItem (textureLayer=0 leaked as text)

Documentation

  • spec/AI_WORKING_SPEC.md — comprehensive architecture doc for AI assistants

Bundle impact

Before After
Framework Custom MVX (5 files, ~500 LOC) React 18 (npm)
Bundle size 667 KB 794 KB (+127 KB)
Visual tests 0 38

How to verify

npm install
npm run build:bundle    # Build the bundle
npm run test:visual     # Run all 38 visual regression tests

Replace the custom MVX rendering framework (BaseComponent, StateStore,
Compositor, MVX orchestrator) with React 18 functional components using
an adapter pattern that preserves the existing public API.

## What changed

- **Adapter pattern**: ReactCaptureMenu and ReactResultView implement the
  exact same public API as the original classes, using ExternalStore +
  useSyncExternalStore for state management instead of MVX state IDs
- **32 React components** replace 31 MVX components across CaptureMenu
  (5 components) and ResultView (18 components + JSONRenderTree)
- **Visual regression tests**: 38 Playwright screenshot tests ensure
  pixel-level visual parity with the original UI
- **SCSS unchanged**: Same 3 SCSS files, same class names, same DOM
  structure — zero CSS changes needed
- **Ace editor**: Wrapped with useRef/useEffect for proper lifecycle
  management (no StrictMode — incompatible with Ace)

## Architecture

- ExternalStore<T> bridges imperative API → React reactivity
- Adapter classes translate method calls → store.setState() → re-render
- React components are purely presentational (read store, call handlers)
- Context pattern passes adapter instance to component tree
- JSONRenderItem discriminated union for recursive JSON tree rendering

## Bug fixes

- hide() now actually works (MVX had truthy-check bug)
- Removed dead IE11 navigator.msSaveBlob code path
- Fixed falsy number rendering in VisualStateListItem (textureLayer=0)

## Bundle impact

- Before: 667 KB (MVX + Ace)
- After: 794 KB (React + ReactDOM + Ace) — +127 KB for React

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
sebavan and others added 2 commits March 20, 2026 15:01
TSLint fixes:
- Add braces to all single-line if/else bodies (curly rule)
- Add public visibility modifiers to ExternalStore members (member-access)
- Remove unnecessary semicolons from arrow function properties (semicolon)
- Add tslint:disable-next-line for react-dom/client submodule imports
- Rename shadowed variable in SourceCode.tsx (no-shadowed-variable)
- Use T[] syntax instead of Array<T> in types.ts (array-type)

Dependency updates:
- React 18 → 19, react-dom 18 → 19
- TypeScript 4.7 → 5.9
- webpack 5.73 → 5.105, webpack-cli 4 → 7
- css-loader 6 → 7, style-loader 3 → 4, sass-loader 13 → 16
- sass 1.53 → 1.98, ts-loader 9.3 → 9.5
- exports-loader 4 → 5, livereload 0.9 → 0.10
- @shaderfrog/glsl-parser 5 → 7
- @types/webxr 0.5.1 → 0.5.24

Webpack config fixes for breaking changes:
- style-loader v4: replace query string syntax with options object
- sass v1.98: add silenceDeprecations for @import warnings

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…node v1 → v4

TypeScript 5.9 uses nullish coalescing (??) which requires Node.js 14+.
The CI was running Node.js 12 which crashes on TypeScript's own source code.

Also update GitHub Actions to v4 to avoid Node.js 20 deprecation warnings.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sebavan sebavan merged commit 914a1d1 into master Mar 20, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants