PreactMVC logo PREACTMVC

PreactMVC

Angular's architecture on Preact — modules, DI, class views, and worker-rendered islands, with the whole Preact ecosystem on tap.

Get started See it live

$ npm install -g @preact-mvc/cli && pmvc new my-app
counter.view.tsx
import { Component, View, inject, signal, computed }
  from '@preact-mvc/core';

@Component()
export class CounterView extends View {
  private session = inject(SessionService);

  count = signal(0);
  doubled = computed(() => this.count.value * 2);

  // render() is plain JSX — signal reads re-render.
  render() {
    return (
      <button onClick={() => this.count.value++}>
        count: {this.count.value}
      </button>
    );
  }
}

The convergence

Angular's architecture, Preact's freedom

Angular strengths and trade-offs on one side, React and Preact strengths and trade-offs on the other, converging into PreactMVC: modules and dependency injection, MVC controllers and lifecycle, signal reactivity, JSX views on Preact, class views plus function components, and the pmvc CLI

The stack

One injector tree, class views on top

Everything Angular gave you — scoping, ownership, lifecycle — rebuilt around signals and JSX instead of decorators-era change detection.

Modules & DI

@Module composes providers across a hierarchical injector. inject() in field initializers resolves nearest-scope-first — Angular's resolution model, verbatim.

Class views

@Component + View: constructed once per mount in their own injector scope. No hooks, no deps arrays, no stale closures — render() is just code.

Controllers

@Controller classes own interaction state. Declaration decides lifetime: transient, view-scoped, or module-shared — the MVC layer Angular implied but never enforced.

Signals

signal/computed/effect with scope-bound disposal — effects created at construction die with the scope. Fine-grained updates, no manual unsubscribe.

Live

Two demo apps, same packages

Both mount from @preact-mvc/* by package name — the same code the docs site documents and the test suite exercises.

Why PreactMVC

Structure without the template compiler

Angular's scoping

Module ownership is enforced at mount: declared components refuse to mount outside their injector scope. Architecture you can't drift away from.

Every Preact library

render() is ordinary Preact vnodes — UI kits, hooks, preact/compat, TanStack Query, ECharts all drop in unchanged. Provide the client via DI; bridge subscribe into a signal.

Worker islands

The same View classes render inside Web Workers via @preact-mvc/atoll-island, streamed to the shell DOM over an op protocol.

Zero compiler magic

No template language, no AOT pass, no zone. Decorators attach metadata the runtime reads — the framework is ~5 small source files of DI and mount logic.

Mission: enterprise structure for the JSX era. PreactMVC keeps the parts of Angular that scaled — injectors, modules, declared ownership, MVC layering — and swaps the rest for signals and class-based JSX on Preact. inject() replaces constructor metadata, signals replace change detection, and worker islands push real UI off the main thread. And because views render as ordinary Preact vnodes, the entire Preact ecosystem — and via preact/compat, much of React's — drops in unchanged. Declare the scope once, and the view tree respects it.

Learn more

Want to dig in?