Signals & effects

Reactivity is Solid's signal graph underneath, wrapped so that effects created inside an injection scope are disposed with it — the piece that makes service-level effect() safe. The API keeps .value reads/writes over Solid primitives.

The primitives

import { signal, computed, effect, batch, untracked } from '@solid-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 inside JSX expressions

A view's render() runs once — Solid's model. Tracking happens where the JSX compiler wraps expressions in computations: a signal read inside a JSX expression, attribute, or <For>/<Show> updates exactly the DOM that read it on write — no re-render, no vdom diff. A signal read eagerly in the render() body (e.g. into a local variable) is a snapshot and stays stale — move it inside the JSX.

Reads in event handlers and timers don't subscribe (they're outside any computation), so you never hit the read-during-handler subscription traps hooks create — timers can safely write signals and the tracked DOM updates itself.

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.