When React introduced Hooks in 2019, it changed the way we thought about building components. Suddenly, we could manage state and side effects in functional components without juggling class lifecycle methods. Hooks like useState
and useEffect
became the bread and butter of modern React development.
But as applications grow, developers are bumping into the limits of Hooks: unnecessary re-renders, complicated dependency arrays, and performance bottlenecks in fine-grained updates.
This is where Signals come in. Signals don’t replace Hooks—they complement them—and offer a fresh approach to reactivity that’s already gaining traction in frameworks like SolidJS, Angular, and even experiments in React’s ecosystem.
A Quick Recap: React Hooks
Here’s the classic way we handle state with Hooks:
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count is {count}
</button>
);
}
This works beautifully. But if this count
state was passed deep into multiple child components, React would need to re-render all of them whenever count
changes—even if they don’t directly use it.
What Are Signals?
A Signal is a special kind of state container.
Think of it like a box that holds a value, and whenever the value changes, only the places that use it update—no unnecessary re-renders.
Here’s the same counter implemented with Signals (example using @preact/signals-react):
import { signal } from "@preact/signals-react";
const count = signal(0);
function Counter() {
return (
<button onClick={() => count.value++}>
Count is {count.value}
</button>
);
}
Notice a few things:
No
useState
.No re-rendering the entire component tree.
Only the parts of the UI that directly read
count.value
update.
Why Signals Complement Hooks
Instead of replacing Hooks, Signals solve some of their shortcomings:
1. Fine-grained reactivity
Hooks: Changing state re-renders the whole component.
Signals: Only the elements that read the signal update.
2. No dependency arrays
With
useEffect
, we manually track dependencies:
useEffect(() => {
fetchData(query);
}, [query]);
With Signals, the system knows what depends on what, so no arrays are needed.
3. Global state without Context
Instead of setting up a React Context and a provider, a signal can be imported and shared anywhere:
export const user = signal(null);
Any component reading user.value
will automatically update when it changes.
Signals + Hooks in Practice
The sweet spot is using both together.
For example, keep Hooks for local component state, and Signals for global/shared reactive data:
import { signal } from "@preact/signals-react";
import { useState } from "react";
const theme = signal("light");
function Toolbar() {
const [open, setOpen] = useState(false);
return (
<div>
<button onClick={() => setOpen(!open)}>Menu</button>
<button onClick={() => theme.value = theme.value === "light" ? "dark" : "light"}>
Toggle Theme
</button>
<p>Theme is: {theme.value}</p>
</div>
);
}
open
(local UI toggle) works great withuseState
.theme
(shared across the app) is better suited for a Signal.
The Road Ahead
React’s own team is experimenting with concepts like React Forget (a compiler that could make Hooks behave more like Signals under the hood). Meanwhile, libraries like @preact/signals-react
are giving us a glimpse of the future.
If Hooks gave us a declarative way to manage state, Signals give us a more efficient and predictable reactivity model. Together, they create a developer experience that’s both powerful and ergonomic.
✨ Closing Thoughts
Hooks aren’t going anywhere. They’re still the foundation of React apps. But Signals add a new layer:
Fewer re-renders
Cleaner reactive code
A simpler way to manage shared state
Think of Hooks as the engine, and Signals as a turbocharger.
The next time you find yourself struggling with complex useEffect
dependencies or heavy global state management, give Signals a try—you may find they make your React codebase feel lighter and faster.