Skip to content

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) // 0
count.value = 3
console.log(doubled.value) // 6

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 component
function 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 tracked
function 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.

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()))
}

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


References