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 subscribing

Where they live

Signal lives onPattern
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:

theme.service.ts
@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.