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