Handling React state with Signals
Discover how Signals can streamline React state management, reducing unnecessary re-renders and boosting application performance.
Photo by Ashim D'Silva under the Unsplash license
As we mentioned back in handling React state with Observables , React’s component-based architecture relies on state to manage dynamic data within an application whicn can lead to performance issues, especially in larger applications, when the state location is too up in React’s component tree.
State management recall
State is often managed at the top of the component tree (e.g., in a parent component or context). This approach allows multiple child components to access and modify the same data, but when state changes in a parent component, all child components re-render by default. This happens even if the specific state change doesn’t affect all children.
The newly proposed TC39 Signals proposal helps by introducing a state management layer that exists independently of React’s render cycle.
An example App
Using React
Let’s use as an example this application that:
- Keeps its state in the top component: a count that increments every 2 seconds and wraps after 2
- App’s children are wrapped by a “Theme” component that will change its border color everytime it is re-rendered
- A child component that displays the state count
Here’s what happens every 2 seconds:
- App updates its count state.
- App re-renders, which causes Theme to re-render.
- Theme generates a new random hue value.
- Child re-renders with the new count value.
The problem is particularly evident with the Theme
component:
- It re-renders unnecessarily every time count changes.
- Each re-render generates a new random hue, causing the border color to change frequently.
- This change is unrelated to the count update and not intentional.
In a larger application, this pattern of unnecessary re-renders can lead to performance issues, especially if Theme or similar intermediate components perform more complex operations or have their own state.
Using Signals
Let’s see how it is implemented with signals:
As with Observables , the Theme
component is not being re-rendered anymore.
What is a Signal
Similar to Observables, Signals are reactive primitives designed for managing state in a simple and efficient manner, well-suited for use in UI frameworks.
Key characteristics of Signals are:
- Pull-based model: Signals operate on a pull-based data flow model. This means that the current value of a Signal is only computed when it’s accessed.
- Automatic dependency tracking: When a computation (like a component render) accesses a Signal, it automatically becomes a subscriber to that Signal.
- Fine-grained reactivity: Only the specific computations that depend on a changed Signal are re-executed, leading to very efficient updates.
- Synchronous updates: When a Signal’s value changes, dependent computations are updated immediately and synchronously.
Signal
While an Observable represents a stream of values, a Signal acts more similar to a box holding a value, and you can ask the box for the value it currently holds:
Signal reactivity
Up until this point all we can do with the Signal we have defined is polling its value. We are missing reactivity. To gain that back we need to introduce the effect
function:
Let’s put together an example:
Poor man’s implementation
Let’s show now how to implement a bare minimum Signal
, effect
, and useSignal
.
Signals vs Observables
Compared to Observables, Signals provide a simpler to use API, offering a more intuitive approach to reactive programming that aligns closely with how developers think about state and its changes. While Observables excel in handling complex asynchronous data flows and event streams, Signals shine in synchronous, UI-centric scenarios, providing fine-grained reactivity with less boilerplate.
Most, if not all, of the benefits of using Signals can be achieved with Observables, albeit with more explicit code and careful implementation. Observables, when properly optimized, can match Signals in performance and efficiency through techniques like lazy evaluation, memoization, and selective updates.
It’s important to note that both patterns have their strengths, and the choice between Signals and Observables often depends on the specific requirements of the application, with Signals generally offering a more straightforward and efficient solution for managing UI state, while Observables remain powerful for handling more complex, often asynchronous, data streams.
Conclusions
The future of Signals looks promising, especially with the potential for native browser implementation. As part of the TC39 proposal process, Signals could become a built-in feature of JavaScript, offering significant advantages. Native browser implementation would provide performance optimizations at the engine level, potentially making Signals even more efficient than current JavaScript implementations.
Furthermore, native Signals could integrate seamlessly with other Web APIs, opening up new possibilities for reactive programming in areas like DOM manipulation, Web Components, and even WebAssembly interop.
While the road to standardization and implementation is long, the potential benefits make Signals an exciting prospect for the future of web development, promising a more intuitive and efficient way to handle reactivity in JavaScript applications.