Preact Signals

Preact Signals — fine-grained reactive state that updates only what changes, without re-rendering entire components.

Signals are Preact’s built-in reactive primitive. Unlike useState, which triggers a full component re-render, Signals track which parts of the UI depend on each value and update only those specific DOM nodes.

Creating Signals

import { signal } from "@preact/signals";

const count = signal(0);
const name = signal("Ada");

// Read the value
console.log(count.value); // 0

// Update the value
count.value = 1;
count.value++; // shorthand for count.value = count.value + 1

Signals in Components

import { signal } from "@preact/signals";
import { render } from "preact";

const count = signal(0);

function Counter() {
  // Access .value in JSX — Preact auto-subscribes
  return (
    <div>
      <p>Count: {count.value}</p>
      <button onClick={() => count.value++}>Increment</button>
    </div>
  );
}
Feature useState signal()
Re-render scope Entire component Only the DOM nodes that read .value
Declaration Inside component Inside or outside component
Update setState(val) signal.value = val
Batching Automatic in React 18 Automatic in Preact
Shared state Requires context or prop drilling Import the signal directly

Computed Signals

Derive values that automatically update when their dependencies change.

import { signal, computed } from "@preact/signals";

const firstName = signal("Ada");
const lastName = signal("Lovelace");

const fullName = computed(() => `${firstName.value} ${lastName.value}`);

console.log(fullName.value); // "Ada Lovelace"

// Updating a dependency recomputes automatically
firstName.value = "Alan";
console.log(fullName.value); // "Alan Lovelace"

Effect

Run side effects when signals change.

import { signal, effect } from "@preact/signals";

const query = signal("");

const dispose = effect(() => {
  console.log("Searching for:", query.value);
  // Runs immediately and on every change to query
});

// Clean up when no longer needed
dispose();

Using Signals with Hooks

import { useSignal, useComputed } from "@preact/signals";

function SearchForm() {
  const query = useSignal("");
  const results = useComputed(() => expensiveFilter(query.value));

  return (
    <div>
      <input
        value={query.value}
        onInput={(e) => { query.value = (e.target as HTMLInputElement).value; }}
      />
      <ul>
        {results.value.map((item) => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}
Hook Description
useSignal(initial) Creates a local signal scoped to the component instance
useComputed(fn) Creates a computed signal scoped to the component instance
useSignalEffect(fn) Like useEffect but auto-tracks signal dependencies

Sharing State Without Context

Since signals are standalone values, you can import them directly — no provider needed.

// store.ts
import { signal } from "@preact/signals";

export const currentUser = signal<User | null>(null);
export const isLoggedIn = computed(() => currentUser.value !== null);

// Login.tsx
import { currentUser } from "./store";

function Login() {
  return <button onClick={() => { currentUser.value = { name: "Ada" }; }}>Log in</button>;
}

// Header.tsx
import { isLoggedIn, currentUser } from "./store";

function Header() {
  return isLoggedIn.value
    ? <p>Hello, {currentUser.value!.name}</p>
    : <p>Please log in</p>;
}

Signal Batching

Multiple signal updates in an event handler are batched automatically.

function movePlayer(dx: number, dy: number) {
  // These updates batch — only one DOM update
  x.value += dx;
  y.value += dy;
}

For async or external updates, use batch:

import { batch } from "@preact/signals";

batch(() => {
  x.value += 10;
  y.value += 5;
});