Quickstart
A routed todo app: one module, one store, one controller, one view. Six files, and injection replaces prop drilling from the start.
1 · Install
npm install @preact-mvc/core preact @preact/signals
npm install -D @preact/preset-vite typescript viteDecorators need two tsconfig flags; JSX compiles to Preact's automatic runtime:
{
"compilerOptions": {
"experimentalDecorators": true,
"useDefineForClassFields": false,
"jsx": "react-jsx",
"jsxImportSource": "preact"
}
}
useDefineForClassFields: falsematters:inject()field initializers must run inside the injection context the constructor opens — class-field semantics would evaluate them too early.
2 · A store
import { Injectable, signal, computed } from '@preact-mvc/core';
export interface Todo { id: number; text: string; done: boolean }
@Injectable()
export class TodoStore {
todos = signal<Todo[]>([]);
remaining = computed(() => this.todos.value.filter(t => !t.done).length);
add(text: string) {
this.todos.value = [
...this.todos.value,
{ id: Date.now(), text, done: false },
];
}
}3 · A controller
import { Controller, inject, signal } from '@preact-mvc/core';
import { TodoStore } from './todo.store';
@Controller()
export class TodoController {
private store = inject(TodoStore);
draft = signal('');
submit() {
const text = this.draft.value.trim();
if (text) this.store.add(text);
this.draft.value = '';
}
}4 · A view
import { Component, View, inject } from '@preact-mvc/core';
import { TodoController } from './todo.controller';
import { TodoStore } from './todo.store';
@Component({ controllers: [TodoController] })
export class TodoView extends View {
private c = inject(TodoController);
private store = inject(TodoStore);
render() {
return (
<section>
<h2>Todos ({this.store.remaining.value} left)</h2>
<input
value={this.c.draft.value}
onInput={(e) => (this.c.draft.value = e.currentTarget.value)}
/>
<button onClick={() => this.c.submit()}>add</button>
<ul>
{this.store.todos.value.map((t) => (
<li key={t.id}>{t.text}</li>
))}
</ul>
</section>
);
}
}5 · The module declares it all
import { Module } from '@preact-mvc/core';
import { TodoStore } from './todo.store';
import { TodoView } from './todo.view';
@Module({
providers: [TodoStore],
components: [TodoView],
bootstrap: [TodoView],
})
export class TodoModule {}6 · Bootstrap
import { bootstrap } from '@preact-mvc/core';
import { TodoModule } from './todo.module';
bootstrap(TodoModule, document.getElementById('app')!);What just happened
bootstrapcreated the app injector fromTodoModule, mounted its bootstrap view inside it, and installed the JSX interop that turns View classes into host components.- The view's injector resolved
TodoStore(declared on the module) and the declaredTodoController— once, at mount.render()is plain code; signals re-render it.
Next: dependency injection for provider forms and scopes, or the router to add routes.