Skip to content

Preact Signals — Reactive State Management with @preact/signals

DodaTech Updated 2026-06-28 5 min read

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

  1. Forgetting .value when reading or writing: Signals are objects. Read with signal.value, write with signal.value = newValue. Forgetting .value gives you the signal object itself.
  2. Mutating signal values directly: signal.value.push(item) doesn't trigger updates. Always assign a new value: signal.value = [...signal.value, item].
  3. Creating signals inside components: Signals should be module-level or created with useRef/useState to persist across renders. Creating them inside a function creates a new signal every render.
  4. Using signals without importing from @preact/signals: The preact/hooks package doesn't include signals. Always install and import from @preact/signals.
  5. Not using computed for derived values: Re-computing derived values manually in render functions loses the Caching benefit of computed.

Practice Questions

  1. What package provides signals for Preact? Answer: @preact/signals. Install with npm install @preact/signals.

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

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

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

Are signals a replacement for all useState usage?

: No. Signals excel for values that update frequently or where fine-grained updates matter. For simple, infrequently updated state, useState is fine.

Do signals work outside Preact components?

: Yes. Signals are framework-agnostic. You can use signal(), computed(), and effect() in plain JavaScript modules.

Do signals integrate with Preact DevTools?

: Yes. The @preact/signals package includes DevTools integration for inspecting signal values during development.

Can I use signals with class components?

: No. Signal integration requires the @preact/signals babel plugin and works best with functional components. Class components should use this.setState.

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