PreactMVC
A frontend framework that takes Angular's architecture and renders it with React's ergonomics: modules, dependency injection, and decorators around class-based JSX views on Preact, with signal reactivity and worker-rendered islands built in.
The idea
Angular got application shape right — modules that own providers, injectors that scope services, decorators that declare intent, an MVC split between views and controllers. React got rendering right — components are values, JSX composes freely, and imports resolve identity instead of a template registry. PreactMVC keeps both: you write JSX with ordinary imports, and organize it with modules and DI.
import { Component, Controller, View, inject, signal } from '@preact-mvc/core';
@Controller()
class CounterController {
count = signal(0);
inc = () => this.count.value++;
}
// A view is a class. Injection happens once at mount — no hooks in render().
@Component({ controllers: [CounterController] })
export class CounterView extends View {
private c = inject(CounterController);
render() {
return <button onClick={this.c.inc}>count: {this.c.count.value}</button>;
}
}Three packages
| Package | What it is |
|---|---|
@preact-mvc/core | The framework: injector, decorators (@Module, @Component, @Injectable, @Controller), View classes, signals, context, router. One dependency: preact. |
@preact-mvc/atoll | Facade over @atolljs/islands + @atolljs/core: the op protocol, the proxy DOM a worker renders into, the worker runtime, the island contracts, and the main-thread driver that replays ops onto real DOM. |
@preact-mvc/atoll-island | The binding package: <Island/> shell components on the main thread, defineViewPolyWorker/viewIslandApp on the worker side. The same View classes render in both places. |
What it borrows from each side
| From Angular | From React/Preact |
|---|---|
| Modules that own providers and declare components | Components are values you import, not registered selectors |
Hierarchical injectors, provider scopes, inject() | JSX everywhere — no template language, no compile step |
Decorators for metadata (@Module, @Component) | Freedom of composition — undeclared views mount anywhere |
| MVC split: controllers own interaction state | Signals for fine-grained reactivity |
Standalone components (@Component imports) | Class construction replaces per-render hook calls |
The whole Preact ecosystem, unchanged
render() returns ordinary Preact vnodes, so anything that works in a Preact app works here — no adapters, no "framework bindings" version of the library:
- Any Preact component — icon sets, UI kits, form controls — drops straight into a view's JSX. Function components keep their hooks;
preact/compatunlocks the React ecosystem the same way it does in any Preact app. - Any signal-aware library — the framework's signals are
@preact/signals, so libraries built on them subscribe with zero bridging. - Anything else is two moves: provide the client object through DI, bridge its
subscribeinto a signal. DOM-bound libraries (charts, editors, maps) get a three-line function-component wrapper — see Integrating libraries.
The enterprise demo proves it in practice: TanStack Query, ECharts, zod, date-fns, lucide icons, and a Tailwind/shadcn component layer all run through those moves. Inside worker islands the same View classes render too — even ECharts runs there, emitting SVG through the proxy DOM.
And one thing neither has
Worker-rendered islands. A <Island app="ticker" worker={...} /> mounts a real PreactMVC view tree inside a Web Worker — the same View classes, signals, and DI — while the shell sees an ordinary element that ticks, emits events, and never blocks the main thread. See Islands.
Where to go next
- Quickstart — first app in ten minutes
- Dependency injection — the scoping model everything builds on
- Views & lifecycle — class-based components
- Islands — worker-rendered UI
- Live demos — running apps embedded in this site