Modules
@Module groups providers, controllers, and declared components into a feature. The module's injector — and the transitive closure of its imports — is the scope everything inside it sees.
import { Module } from '@preact-mvc/core';
import { SharedModule } from '../shared/shared.module';
import { TodoStore } from './todo.store';
import { TodoController } from './todo.controller';
import { TodoView } from './todo.view';
@Module({
imports: [SharedModule], // its providers/components join our scope
providers: [TodoStore], // one TodoStore per TodoModule injector
controllers: [TodoController], // shared down the module's subtree
components: [TodoView], // declares scope ownership (see below)
bootstrap: [TodoView], // what a route renders for this module
})
export class TodoModule {}What each field does
| Field | Effect |
|---|---|
providers | Registered in the module's injector — one instance per module scope, shared by everything mounted under it. |
imports | The imported module's providers and declared components become visible in this scope. The closure is transitive: importing a module that imports SharedModule brings shared services along. |
controllers | Registered in the module injector — the same controller instance serves every view in the module's subtree (see Controllers). |
components | Scope ownership, not a lookup table: a declared view may only mount inside this module's injector ancestry. Undeclared views mount anywhere. See below. |
bootstrap | The view rendered when a route points at this module ({ path: '/todos', module: TodoModule }) or when it's the app root. |
Module injectors are cached per parent
getModuleInjector(Mod, parent) creates the injector once per parent scope — two routes into the same module share one injector, so module-scoped services survive navigation inside the feature.
Declaration = scope ownership, not resolution
In JSX, <TodoView/> is already the class — there's no selector registry for a declaration to feed. So components answers a different question: what scope does this view live in?
// TodoView is declared by TodoModule. Mounting it outside that
// module's injector ancestry fails at mount — before construction:
//
// TodoView is declared by TodoModule but mounted outside its
// injector scope. Mount it under a route/module that includes
// TodoModule.
//
// The view's error boundary catches it locally: the rest of the app
// keeps rendering, the failed subtree shows a retryable fallback.Undeclared views are free agents — <HelloView/> works anywhere its imports resolve, Angular's standalone model before standalone existed. Declaring buys you the discipline: the framework guarantees the view's dependency graph is satisfiable by construction, and fails fast with a clear message when it isn't.
exportsexists in the metadata for forward-compat but is not yet enforced — any component in the import closure is mountable in scope.
A feature module under a route
export const routes: Route[] = [
{ path: '/', component: HomeView },
{ path: '/todos', module: TodoModule }, // renders TodoView inside TodoModule's injector
{ path: '/user/:name', component: HelloView },
{ path: '*', component: NotFoundView },
];Module routes are how feature scoping composes with the router — see Router.