Learn how Preact’s Signals bring fine-grained reactivity to React, replacing useState and useEffect with a faster, more efficient state management model.
If you’ve been building with React for a while, you’re probably familiar with the usual duo: useState
and useEffect
. They’ve been with us through thick and thin — handling everything from toggling modals to fetching data. But as your apps get bigger, more dynamic, and performance-sensitive, you start to feel the cracks. Too many re-renders, deeply nested state updates, and messy side effects make things harder than they should be.
That’s exactly where React Signals come into play — offering a new, more reactive way to handle state without the boilerplate, performance pitfalls, or re-render headaches.
A Signal is like a reactive value. You can think of it as a container for data that automatically tracks who’s using it — and when it changes, only those parts get updated. No more guessing whether a useEffect
dependency array is correct or worrying about unnecessary renders.
Signals were popularized by Preact — a lightweight React alternative — through the @preact/signals
library. Preact’s signals model focuses on fine-grained reactivity and optimal performance. More recently, the concepts behind Preact Signals are being explored in React via the @preact/signals-react
package, which acts as a compatibility layer. However, native support for Signals is not yet part of React core.
So when we talk about React Signals today, we’re referring to Preact’s implementation running inside a React environment via this package.
Preact signals have already proven to be extremely efficient in production environments. They use pull-based dependency tracking, meaning that when a component accesses a signal, it registers that access. When the signal updates, only the directly affected computation or DOM patch is re-run. No unnecessary component trees get re-rendered.
This granular tracking is at the core of Signals — and it's the reason why they're so powerful.
At a high level, Signals work like this:
You create a signal with an initial value.
Any component or computation that reads the signal gets automatically subscribed.
When the signal’s value changes, only those subscribers are notified.
Under the hood, signals use a reactive graph. Each signal maintains a list of subscribers (aka effects or components). When the signal’s value is changed via a setter, only the subscribers that depend on it are re-evaluated.
This means that your React component doesn’t have to re-render entirely. Instead, just the expressions or DOM elements that rely on the signal get patched. It’s more like updating a single part of the DOM, not the whole virtual DOM tree.
Contrast this with React’s typical rendering strategy, where updating state with useState
or context triggers the component to re-render (and sometimes its children too).
Here’s a super simple example:
import { signal } from '@preact/signals-react';
const counter = signal(0);
function Counter() {
return <p>Count: {counter.value}</p>;
}
function Button() {
return <button onClick={() => counter.value++}>Increment</button>;
}
In this setup, only the part of the component that reads the signal (<p>
) will update. Not the entire component. Not even the Button
if it's part of the same component.
This is true fine-grained reactivity.
Fine-grained reactivity: Only parts of the UI that depend on the signal re-render or update.
Cleaner logic: No need for useEffect
just to sync values or watch for changes.
Better performance: Reduces unnecessary re-renders, especially in large trees or deep component hierarchies.
Simplified mental model: You read and write directly to signal.value
. That’s it. No more juggling multiple hooks or memoization.
No stale closures: Unlike useState
, signal functions don’t suffer from closure-related bugs.
Built-in tracking: Signals automatically track dependencies, so effects are only re-run when needed.
useState
and useEffect
Let’s be honest — useEffect
often ends up doing too much. Tracking state, syncing DOM, fetching data, debouncing inputs... it’s everywhere.
Signals aim to reduce that need. Instead of manually wiring up effects to listen to state changes, you simply read from a signal, and it just works.
Compare this:
const [name, setName] = useState('');
useEffect(() => {
localStorage.setItem('name', name);
}, [name]);
With Signals:
const name = signal('');
effect(() => {
localStorage.setItem('name', name.value);
});
No dependency arrays, no surprises. effect()
automatically tracks the reactive reads inside it and re-runs only when name.value
changes.
effect()
and computed()
Two core utilities that work hand-in-hand with signals are effect()
and computed()
.
effect()
: Runs a function whenever any signal it reads changes. It’s similar to useEffect
, but smarter — it automatically tracks dependencies based on usage.
effect(() => {
console.log("The counter is", counter.value);
});
No dependency array required — the effect re-runs whenever counter.value
changes.
computed()
: Creates a derived signal based on other signals. It’s a read-only signal that recalculates only when dependencies change.
const doubled = computed(() => counter.value * 2);
This doubled
signal updates only when counter.value
changes, and it can be used in components just like a regular signal.
These utilities allow you to build reactive logic that’s easy to follow and performant by default.
In benchmarks comparing @preact/signals
to React’s default state management:
Signals showed up to 10x faster updates in UI-heavy apps.
Memory usage dropped significantly, especially in apps with large trees and frequent updates.
Component update count was drastically reduced, with fewer full re-renders and more direct DOM patching.
In real-world apps like dashboards, collaborative editors, or animation-heavy interfaces, these performance wins make a noticeable difference.
Real-time dashboards or data-heavy UIs
Apps with complex, interdependent state logic
Live previews or text editors
Dynamic forms with dependent inputs
Scenarios where you want highly performant updates with minimal re-renders
Signals can also reduce the need for React Context or overuse of useMemo
, useCallback
, and React.memo
. Because updates are localized and only affect what really needs updating, you don't have to wrap everything in memoization hacks.
For global or shared state, you can simply export and use signals directly. No provider trees required:
// signals/store.ts
export const theme = signal<'light' | 'dark'>('light');
// Any component
import { theme } from './signals/store';
function ToggleTheme() {
return <button onClick={() => theme.value = theme.value === 'light' ? 'dark' : 'light'}>Toggle</button>;
}
No need for context providers. Just import and go.
Signals offer a refreshing approach to state in React. They bring us closer to true reactivity, with less boilerplate and better performance. Originally implemented by Preact, and now crossing over into the React ecosystem via compatibility layers, Signals represent a fundamental shift in how we think about state and reactivity.
If you’re tired of fighting useEffect
, or just want a more efficient way to build, it’s worth keeping an eye on Signals. React is evolving — and with Signals, it’s getting a lot smarter.
Bonus: Signals are already being explored in libraries like @preact/signals
, @preact/signals-react
, and reactivity
. Dive in and start experimenting!
- Jagadhiswaran Devaraj
📢 Stay Connected & Dive Deep into Tech!
🚀 Follow me for hardcore technical insights on JavaScript, Full-Stack Development, AI, and Scaling Systems:
🐦 X (Twitter): jags
✍️ Read more on Medium: https://medium.com/@jwaran78
💼 Connect with me on LinkedIn: https://www.linkedin.com/in/jagadhiswaran-devaraj/
Let’s geek out over code, architecture, and all things in tech! 💡🔥
Join Jagadhiswaran on Peerlist!
Join amazing folks like Jagadhiswaran and thousands of other people in tech.
Create ProfileJoin with Jagadhiswaran’s personal invite link.
3
5
1