Signals & effects
Reactivity is @preact/signals underneath, wrapped so that effects created inside an injection scope are disposed with it — the piece that makes service-level effect() safe.
The primitives
import { signal, computed, effect, batch, untracked } from '@preact-mvc/core';
const count = signal(0);
const doubled = computed(() => count.value * 2);
count.value++; // write → subscribers recompute
doubled.value; // 2 — computed lazily, cached
batch(() => { // one notification for many writes
firstName.value = 'Ada';
lastName.value = 'Lovelace';
});
untracked(() => state.value); // read without subscribingWhere they live
| Signal lives on | Pattern |
|---|---|
Service (@Injectable) | Domain state — shared by every consumer of the scope |
Controller (@Controller) | Interaction state — drafts, selections, UI toggles |
View (View fields) | Local display state — expanded, hovered, editing |
effect() is scope-aware
Calling effect(fn) during construction — inside an injectable, controller, or view — binds it to that scope's DestroyRef: it stops when the scope deactivates. No manual unsubscribe, no leaks across navigation:
@Injectable({ providedIn: 'root' })
export class ThemeService {
theme = signal<Theme>(readStored());
constructor() {
// Persists on every change; auto-disposed if the scope dies.
effect(() => {
localStorage.setItem('theme', this.theme.value);
document.documentElement.classList.toggle('dark', this.isDark());
});
}
}Outside an injection context, effect() behaves like the upstream primitive — it runs until you call the returned dispose function.
Views subscribe through render()
A view's render() runs inside a tracked scope: any signal read there — a service's, a controller's, its own — re-renders that view on write. Nothing else re-renders; updates are fine-grained by construction. Reads in event handlers and timers don't subscribe (they're outside the tracked scope), so you never hit the read-during-handler subscription traps hooks create.
The same view classes render inside worker islands — signals work identically on the worker thread, and each re-render serializes to ops the shell replays. See Islands.