docs/latest/concepts/signals.md
Signals are Preact's reactive
primitive for managing state in islands. When a
signal's value changes, any component that reads it re-renders automatically -
no need for setState or manual subscriptions.
Use useSignal inside a component for local state, or signal at module level
for shared state:
import { useSignal } from "@preact/signals";
export default function Counter() {
const count = useSignal(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => count.value++}>Increment</button>
</div>
);
}
[info]: Signals can be rendered directly in JSX (
{count}) without accessing.value. Preact detects the signal and subscribes to updates automatically.
Use computed to derive values from other signals. Computed signals update
automatically when their dependencies change:
import { useComputed, useSignal } from "@preact/signals";
export default function TemperatureConverter() {
const celsius = useSignal(20);
const fahrenheit = useComputed(() => celsius.value * 9 / 5 + 32);
return (
<div>
<input
type="range"
min={-40}
max={100}
value={celsius}
onInput={(e) => celsius.value = Number(e.currentTarget.value)}
/>
<p>{celsius}°C = {fahrenheit}°F</p>
</div>
);
}
Signals can be passed as props to islands. Fresh automatically serializes them on the server and reconstructs them as live signals on the client:
import { useSignal } from "@preact/signals";
import Slider from "@/islands/Slider.tsx";
export default function Home() {
const value = useSignal(50);
return (
<div>
<Slider value={value} />
<Slider value={value} />
</div>
);
}
Both sliders share the same signal - moving one updates the other. When the same signal object is passed to multiple islands, Fresh preserves the reference so they stay synchronized.
[info]: Using
useSignalin a route component (not an island) is intentional here. The signal is created during server rendering, serialized into the HTML, and reconstructed as a live signal on the client. This is how Fresh shares reactive state between multiple islands on the same page.
For state that needs to be shared between unrelated islands, create a signal in a separate module:
import { signal } from "@preact/signals";
export const cart = signal<string[]>([]);
import { cart } from "@/utils/cart.ts";
export default function AddToCart(props: { product: string }) {
return (
<button onClick={() => cart.value = [...cart.value, props.product]}>
Add {props.product}
</button>
);
}
import { cart } from "@/utils/cart.ts";
export default function CartCount() {
return <span>Items in cart: {cart.value.length}</span>;
}
Since both islands import the same module-level signal, they share the same state automatically. See Sharing state between islands for more patterns.
When signals are passed as island props, Fresh handles serialization automatically:
.peek()signal() or
computed()The signal's inner value must itself be serializable (see Islands - Passing props for the full list of supported types).