Skip to content

Migrate frontend from React to Svelte #82

Description

@droarty

Plan: Migrate Frontend from React to Svelte

Context

The apps/web/ directory is a React 19 + React Router v6 SPA bundled with esbuild via Nx. The goal is to replace it entirely with a Svelte 5 + SvelteKit (SPA mode) application while preserving all existing behavior — the same routes, the same WebSocket/document-model integration, the same Tailwind styling, and the same backend API.

Three pure-JS services (EventManager.ts, documentModelStore.ts, api.ts) have no React dependencies and can be reused as-is. Everything else — 43 components, hooks, contexts, and registries — needs to be rewritten in Svelte.


Technology Choices

Concern React (current) Svelte (target)
Framework React 19 Svelte 5
Routing React Router v6 SvelteKit (SPA mode via @sveltejs/adapter-static)
Build @nx/esbuild:esbuild @sveltejs/kit + Vite
State (global) React Context Svelte writable stores
State (external) useSyncExternalStore Native $store / subscribe protocol
Lazy components React.lazy + Suspense Dynamic import() + {#await}
UI primitives Radix UI Bits UI (bits-ui) — headless Svelte components
Styling Tailwind + CVA + clsx Same (unchanged)
Icons lucide-react lucide-svelte

Why SvelteKit SPA mode? It is the standard Svelte app scaffold (Vite-based, active ecosystem) and its file-based routing cleanly replaces React Router. Disabling SSR with adapter-static + prerender = false gives a pure SPA — identical to the current app.


Phase 1 — Scaffold the Svelte App

1a. Replace apps/web/ with SvelteKit

  • Rename apps/web/apps/web-react/ (keep as reference during migration)
  • Scaffold new apps/web/ with npx sv create selecting:
    • SvelteKit (Vite)
    • TypeScript
    • Tailwind CSS
    • No SSR (adapter-static, prerender = false)
  • Update apps/web/project.json (Nx targets) to delegate to vite build / vite dev
  • Copy path aliases from old tsconfig.app.json:
    • @/*apps/web/src/*
    • @multiplayer-base/shared-typeslibs/shared-types/src/index.ts
  • Copy tailwind.config.js and postcss.config.js from old app

1b. Install new dependencies

bits-ui          # headless Svelte component primitives (replaces Radix UI)
lucide-svelte    # icon library
class-variance-authority  # keep as-is
clsx + tailwind-merge     # keep as-is
axios            # keep as-is

Remove: react, react-dom, react-router-dom, all @radix-ui/*, lucide-react


Phase 2 — Migrate Pure-JS Services (no changes needed)

Copy these files verbatim into apps/web/src/lib/services/:

  • EventManager.ts — WebSocket singleton
  • documentModelStore.ts — External store (already uses a subscribe interface compatible with Svelte stores)
  • api.ts — Axios HTTP client

Phase 3 — Auth Store (replaces AuthContext)

New file: apps/web/src/lib/stores/auth.ts

Replace AuthContext.tsx with a plain Svelte writable store. Components read with $authStore.user instead of useAuth(). PrivateRoute becomes a SvelteKit layout guard in +layout.svelte.


Phase 4 — Routing (SvelteKit file-based)

src/routes/
├── +layout.svelte
├── +page.svelte            # redirect to /dashboard
├── login/+page.svelte
├── register/+page.svelte
├── auth/callback/+page.svelte
└── dashboard/
    ├── +layout.svelte      # PrivateRoute guard
    ├── +page.svelte
    ├── user/+page.svelte
    ├── author/+page.svelte
    ├── admin/+page.svelte
    └── settings/+page.svelte

Phase 5 — Core Components

  • LayoutDocumentView.svelte — replace useSyncExternalStore with Svelte subscription + onMount/onDestroy
  • LayoutRenderer.svelte — replace React.lazy+Suspense with dynamic import()+{#await}; pure-JS utilities copy unchanged
  • layoutRegistry.ts — replace React.lazy() with plain () => import() returning a Promise
  • TargetPortal — custom Svelte portal via document.getElementById + tick() or Svelte 5 mount()
  • PageHeader.svelte — straightforward rewrite

Phase 6 — UI Components (Radix → Bits UI)

Old (Radix) New (Bits UI)
accordion.tsx bits-ui Accordion.*
tabs.tsx bits-ui Tabs.*
button.tsx bits-ui Button or custom
tree-view.tsx Rewrite with Bits Accordion

Phase 7 — Layout Components

10 layout components rewritten as .svelte files. Key translations:

  • useRef scroll → bind:this + afterUpdate
  • bind:value replaces controlled inputs
  • useState + Context → writable store via Svelte context
  • useMemo$: derived statements

Phase 8 — Pages

7 pages rewritten as SvelteKit +page.svelte files using goto() for navigation and $page.url.searchParams for query params.


Phase 9 — Message Components

6 message components rewritten as Svelte components using the Svelte portal solution.


Phase 10 — Registries

Update lazy import syntax in layoutRegistry.ts, messageRegistry.ts, documentRegistry.ts.


Files NOT changed

  • apps/api/ — untouched
  • libs/shared-types/ — untouched
  • apps/api-e2e/ — untouched

Verification

  1. npm run restart:web — dev server starts on :4200
  2. Login/register flows work
  3. Google OAuth completes end-to-end
  4. User dashboard loads with layout from workflow JSON
  5. Chat sends and receives messages via WebSocket
  6. Admin page lists users, role update works
  7. Settings page saves changes
  8. npx nx test api — backend tests still pass
  9. Manual smoke test of all 8 routes

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions