Preact Signals — Reactive State Management with @preact/signals
In this tutorial, you will learn about Preact Signals. We cover key concepts, practical examples, and best practices to help you master this topic.
Learn Preact Signals: the reactive state primitive from @preact/signals that enables fine-grained reactivity without virtual DOM overhead in Preact applications.
In this lesson, you'll understand what signals are, how they differ from hooks-based state, and how to use them for efficient state management.
What You'll Learn
How to create signals, read and write their values, use computed signals, and integrate signals with Preact components for automatic re-rendering.
Why It Matters
Signals provide fine-grained reactivity. When a signal changes, only the specific DOM nodes bound to that signal update — not the entire component tree. This eliminates unnecessary re-renders.
Real-World Use
Durga Antivirus Pro's real-time threat counter uses signals to track scan results. When a new threat is found, only the counter text updates without re-rendering the entire dashboard.
flowchart LR
A[Signal] -->|Value Changes| B[Computed Signal]
A --> C[Component A - Text Node]
A --> D[Component B - Counter]
B --> E[Component C - Derived Value]
style A fill:#673ab8,color:#fff
style B fill:#4a148c,color:#fff
Installing Signals
Signals require the @preact/signals package:
npm install @preact/signals
Import the core primitives:
import { signal, computed, effect } from '@preact/signals';
Creating and Using Signals
A signal is a reactive value container:
import { signal } from '@preact/signals';
import { render } from 'preact';
const count = signal(0);
function Counter() {
return (
<div>
<p>Count: {count}</p>
<button onClick={() => count.value++}>Increment</button>
</div>
);
}
render(<Counter />, document.getElementById('app'));
Output: Clicking "Increment" updates the displayed count. Accessing count directly in JSX automatically subscribes the component to changes. Only the text node containing the count value updates — not the entire component.
Computed Signals
Derived values that update automatically when dependencies change:
import { signal, computed } from '@preact/signals';
const items = signal([
{ name: 'Apple', price: 1.5 },
{ name: 'Banana', price: 0.75 },
{ name: 'Cherry', price: 2.0 }
]);
const taxRate = signal(0.08);
const total = computed(() => {
const subtotal = items.value.reduce((sum, item) => sum + item.price, 0);
return subtotal + subtotal * taxRate.value;
});
function Cart() {
return (
<div>
<p>Total (with tax): ${total.value.toFixed(2)}</p>
<button onClick={() => {
items.value = [...items.value, { name: 'Date', price: 3.0 }];
}}>Add Item</button>
</div>
);
}
Output: Adding an item automatically updates the total. computed caches its result and only re-computes when items or taxRate change.
Effects with Signals
Run side effects when signals change:
import { signal, effect } from '@preact/signals';
const theme = signal('light');
const fontSize = signal(16);
effect(() => {
document.body.style.backgroundColor = theme.value === 'light' ? '#fff' : '#222';
document.body.style.color = theme.value === 'light' ? '#000' : '#fff';
document.body.style.fontSize = `${fontSize.value}px`;
});
function ThemeSwitcher() {
return (
<div>
<button onClick={() => theme.value = theme.value === 'light' ? 'dark' : 'light'}>
Toggle Theme
</button>
<button onClick={() => fontSize.value += 2}>Increase Font</button>
</div>
);
}
Output: Toggling the theme changes the page background and text color. Changing font size updates the text size. The effect runs immediately and re-runs whenever theme or fontSize change.
Signals vs useState
Signals offer a different mental model than hooks:
// useState approach — entire component re-renders
function CounterUseState() {
const [count, setCount] = useState(0);
// Every state change re-renders the entire component function
return (
<div>
<p>Count: {count}</p>
<ExpensiveChild />
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
);
}
// Signal approach — only the text node updates
function CounterSignal() {
const count = signal(0);
return (
<div>
<p>Count: {count}</p>
{/* ExpensiveChild does NOT re-render when count changes */}
<ExpensiveChild />
<button onClick={() => count.value++}>+</button>
</div>
);
}
Output: With signals, ExpensiveChild never re-renders when the count changes. The signal binding updates the text node directly without going through the component's virtual DOM diff.
Common Mistakes
- Forgetting
.valuewhen reading or writing: Signals are objects. Read withsignal.value, write withsignal.value = newValue. Forgetting.valuegives you the signal object itself. - Mutating signal values directly:
signal.value.push(item)doesn't trigger updates. Always assign a new value:signal.value = [...signal.value, item]. - Creating signals inside components: Signals should be module-level or created with
useRef/useStateto persist across renders. Creating them inside a function creates a new signal every render. - Using signals without importing from
@preact/signals: Thepreact/hookspackage doesn't include signals. Always install and import from@preact/signals. - Not using computed for derived values: Re-computing derived values manually in render functions loses the Caching benefit of
computed.
Practice Questions
What package provides signals for Preact? Answer:
@preact/signals. Install withnpm install @preact/signals.How do you create a derived value from one or more signals? Answer: Use
computed(() => expression)which returns a read-only signal that updates when its dependencies change.What is the advantage of signals over useState? Answer: Fine-grained reactivity. Only the DOM nodes bound to the signal update, not the entire component tree. This eliminates unnecessary re-renders.
How do you subscribe to signal changes for side effects? Answer: Use the
effect()function. It re-runs the callback whenever any signal accessed inside it changes.
Challenge
Build a spreadsheet-like cell system where cells contain signals, and computed signals derive values from other cells. Demonstrate that changing one cell updates all dependent cells automatically.
Mini Project
Create a shopping cart with signals: cartItems signal for items, computed for subtotal/tax/total, and effect to persist the cart to localStorage on every change.
FAQ
What's Next
Learn about Preact Computed Values and Effects with Signals for deeper coverage of computed signals and effect management.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro