Signals
A signal is a value container with built-in subscriptions. When you read a signal inside a reactive context (a component, a computed, an effect), the framework records the dependency. When you write to the signal, the framework notifies only the subscribers that depend on it — not the entire component tree.
import { signal, computed } from '@preact/signals'
const count = signal(0)const doubled = computed(() => count.value * 2)
console.log(doubled.value) // 0count.value = 3console.log(doubled.value) // 6How signals differ from useState
Section titled “How signals differ from useState”useState in React triggers a re-render of the entire component. React diffs the virtual DOM against the previous render and commits changes to the real DOM. Signals skip the virtual DOM entirely — they update the DOM nodes directly when their value changes.
// React — changing count re-runs the whole componentfunction Counter() { const [count, setCount] = useState(0) return ( <div> <span>{count}</span> <span>{Date.now()}</span> {/* re-created every render */} </div> )}
// SolidJS — each signal subscription is independently trackedfunction Counter() { const [count, setCount] = createSignal(0) return ( <div> <span>{count()}</span> <span>{Date.now()}</span> {/* created once */} </div> )}In SolidJS, reading a signal inside a JSX expression creates a subscription between that signal and the exact DOM text node. Changing the signal updates only that text node. React would re-execute Counter, run the entire JSX, diff the virtual DOM, and then patch the real DOM.
Fine-grained reactivity in practice
Section titled “Fine-grained reactivity in practice”Preact Signals can be used with any framework, including React. When a signal changes, the component subscribes to the signal directly and schedules a targeted update rather than a full re-render.
import { signal } from '@preact/signals-react'
const count = signal(0)
function Counter() { return ( <button onClick={() => count.value++}> {count} </button> )}Angular Signals (Angular 17+) provide the same pattern inside Angular’s change detection. Angular developers previously relied on zone.js to detect changes by patching browser APIs. Signals give explicit, opt-in reactivity.
import { signal, computed } from '@angular/core'
@Component({ ... })export class SearchComponent { query = signal('') results = computed(() => this.search(this.query()))}Direct mutation libraries
Section titled “Direct mutation libraries”Valtio and Legend-State take a different approach: you mutate a proxy object, and the library figures out what changed.
import { proxy, useSnapshot } from 'valtio'
const state = proxy({ count: 0, text: 'hello' })
function Counter() { const snap = useSnapshot(state) return <button onClick={() => ++state.count}>{snap.count}</button>}Valtio wraps your state in a Proxy that intercepts reads and writes. useSnapshot returns a frozen copy and subscribes to the exact paths the component accessed. Mutation looks like plain JavaScript — state.count++ — but triggers updates only where snap.count is read.
Signals compared to React mental model
Section titled “Signals compared to React mental model”Signals invert React’s contract. React says “the render function is the truth — run it to find out what changed.” Signals say “the value is the truth — track exactly who reads it and notify them when it changes.” The signal approach eliminates the virtual DOM and reduces unnecessary computation. The cost is that dependency tracking is implicit — a signal read inside a callback that never executes will not be tracked, which can cause stale closures if you are not careful.
Try it yourself
Section titled “Try it yourself”References