Re-render Worries and the Solution from Signals
As you work on larger React projects, the pain of re-renders becomes more evident. The core issue lies in React’s update mechanism. When you change a useState in a parent component, the entire child component tree is often pulled into the re-render cycle, even if they don’t use that data.
Take a stock dashboard project I was involved in as an example. With over 500 stock tickers updating prices every second, using useState combined with the Context API kept the browser CPU at 80-90%. FPS plummeted to 15-20, causing noticeable lag. After I proposed switching to Signals, the FPS stabilized at a smooth 60. My team of five developers also breathed a sigh of relief as they no longer had to write complex useMemo or useCallback hooks to block redundant renders.
Signals are not a new concept, as they are already well-known in SolidJS or Preact. However, when introduced to React, they are a complete game-changer. Instead of re-rendering the entire component, Signals directly update the value at the specific location in the DOM. Imagine only needing to change a number on the screen without disturbing the entire massive component tree.
Installing Signals into Your Project
To get started, we will use the @preact/signals-react library. This is currently the most stable solution for bringing the Signal mechanism into the React ecosystem. Open your terminal and run:
npm install @preact/signals-react
A small note for those using Vite or Next.js: To achieve maximum performance and automatic change tracking, you should configure an additional Babel plugin. However, for getting started, installing the core library is enough to experience the difference.
Real-world Implementation
1. Initializing a Signal
Instead of useState, you use the signal() function. The special thing is that you can declare it outside the component to create an extremely lightweight global state.
import { signal } from "@preact/signals-react";
// Declared outside the component, accessible from any file
const count = signal(0);
function Counter() {
return (
<div>
<p>Click count: {count.value}</p>
<button onClick={() => count.value++}>Increase now</button>
</div>
);
}
2. Optimization with Computed Signals
When a value depends on another Signal, use computed. It works similarly to useMemo but is smarter: it only recalculates when the source data actually changes.
import { signal, computed } from "@preact/signals-react";
const cartItems = signal([{ id: 1, price: 150000 }, { id: 2, price: 200000 }]);
// Automatically calculate total price whenever the cart updates
const totalPrice = computed(() =>
cartItems.value.reduce((acc, item) => acc + item.price, 0)
);
function ShoppingCart() {
return <h2>Total payment: {totalPrice.value} VND</h2>;
}
3. Managing Side Effects with effect
The effect() function replaces useEffect. You don’t need to declare a dependency array [] because it automatically knows which Signals you are using inside to re-run when necessary.
import { signal, effect } from "@preact/signals-react";
const theme = signal("dark");
effect(() => {
console.log(`Theme changed to: ${theme.value}`);
document.body.className = theme.value;
});
Verifying Real Performance
How do you know if Signals are actually effective? Open the Profiler tab in React DevTools.
With useState, every time the state changes, the component is “highlighted” to signal a re-render. With Signals, when you change count.value, you will see the component containing it remain completely still. Signals directly manipulate the text node in the DOM, bypassing React’s heavy Virtual DOM diffing process.
Lessons Learned in Large-Scale State Management
- File Structure: You should group Signals into a
store/directory. SeparateauthSignal.jsandcartSignal.jsfor easier maintenance. - Use appropriately: Don’t completely abandon
useState. For simple forms or state used only within a single component,useStateremains a lean choice. - Avoid loops: Always carefully check your
effectfunctions to avoid situations where Signal A updates Signal B, which then updates A, causing the browser to hang.
Tracing render bugs in large applications is often very time-consuming. Signals help decouple data logic from the component lifecycle, making the code much easier to read and debug. If your project is facing performance issues, try introducing Signals into a small module. You will see the difference immediately.

