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 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 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.