Signals Are Old News That Finally Stuck

The term "signals" may feel like a fresh buzzword, but the underlying ideas trace back decades. What began as academic groundwork in the 1970s and matured through computer science research in the 1990s and early 2000s found its first mainstream web expression in KnockoutJS. After that initial wave, signals faded from the collective frontend consciousness—until recently, when a new generation of libraries brought them back with different names and slightly different implementations.

Today, what we loosely call signals covers a spectrum of similar approaches: MobX observable states, Vue.js refs and shallow refs, and SolidJS signals, to name a few. These all fall under the umbrella of fine-grained reactivity, even though each library draws the line between "fine" and "coarse" updates at a different point. The practical outcome is that nearly every major UI framework—SolidJS, Marko, Vue.js, Qwik, Angular, Svelte, Wiz, Preact—has adopted some variant of this pattern. The notable holdout is React.

At its core, a signal pairs an accessor (getter) with a setter. The setter updates the stored value and notifies all dependent effects. The accessor retrieves the value and is re-run by those effects whenever an upstream change occurs.

Pull vs. Push: A Necessary Tradeoff

Understanding why signals matter requires examining how systems manage data flow. Two fundamental architectures exist:

  • Pull: The consumer asks the source for updates.
  • Push: The source sends updates as soon as they're available.

Pull systems must poll or otherwise schedule checks to keep data current. They also face the "state tearing" problem—a scenario where different parts of the UI hold inconsistent views of the same state, such as a header showing 8 posts while the list beneath it renders 10.

Push systems avoid pollings entirely, but the source has no knowledge of whether the consumer can handle incoming updates. This creates backpressure: when a data source floods a consumer faster than it can process, data packages get lost (leading back to state tearing) or, worse, the client crashes.

In pull-based systems, the accepted tradeoff is that data doesn't know where it's consumed, forcing the receiving side to implement precautions like React's teardown/re-render cycle. In push-based systems, the data knows its consumers but the receiving side must manage the update stream carefully, as RxJS demonstrates. The pull approach excels at scheduling updates efficiently; the push approach offers granular control over where data is used. The natural next step is a hybrid.

Push-Pull: The Reactive Middle Ground

A push-pull system maintains a subscriber list for each piece of state. When the state changes, subscribers are notified—but they don't receive the new data itself. Instead, they're told their current value is stale. Each subscriber then re-fetches at an appropriate time, avoiding backpressure while behaving like a traditional pull mechanism. The key difference: this re-fetch only happens when the subscriber knows there's genuinely new data.

These tracked data sources are what we call signals, and the subscriber mechanisms that react to staleness are effects. This designation shouldn't be confused with React's useEffect, which shares a name but follows a completely different model.

What Makes Reactivity "Fine-Grained"

A reactive system exists only when data can notify its consumer of changes and the consumer can apply them. To qualify as fine-grained, two conditions must hold:

  1. Efficiency: The system runs only the minimum necessary computations.
  2. Glitch-Free: No intermediate states appear during a state update.

Efficiency in UI frameworks comes down to the accessor pattern. Because a signal is a getter function rather than a plain variable, the value lives outside the component's scope—templates receive a reference to a getter, and when effects execute, they pull a fresh value. In Solid, for instance, a signal-based component doesn't need to re-execute its entire body on update; only the signal's getter re-runs, keeping computation strictly limited to what changed.

Eliminating intermediary states is trickier. Non-reactive systems sidestep the problem by tearing down potentially stale artifacts and rebuilding from scratch. Reactive systems must solve what's known as the Diamond Challenge. Imagine a node E that depends on two parents: D and B. D alone depends on C, which depends on A. If A changes, an over-eager system could propagate the update to B and then immediately to E, while D is still holding staleness—forcing E into an intermediary state that shouldn't exist.

Framework implementations resolve this with one of two strategies:

  1. Lazy signals: A scheduler orders all updates (AB/CDE), guaranteeing correctness by sequence.
  2. Eager signals: Each signal tracks whether its parents are stale, checking, or clean. When E receives B's update, it triggers a verification on D, which climbs the dependency tree until it can crawl back to a cleaned state before allowing E to update.

The Practical Payoff

The abstract properties of fine-grained reactivity translate directly to developer experience. Code that defaults to being glitch-free and efficient removes a category of bugs we've learned to chase by hand. The mental overhead of coordinating dependent state drops because the framework enforces a cleaner connection to the platform with less abstraction in between.

That said, familiarity carries its own weight. Developers are most productive in paradigms they already understand, and established tools have weathered countless edge cases. Innovation requires cognitive rework—the shift from jQuery-based imperative DOM updates to JSX was uncomfortable until it normalized. The same adjustment applies to signal-driven rendering, and it will pass just as those earlier transitions did. Each library's implementation strategy for lazy versus eager updates, scheduling, DOM interaction, and debugging will shape how painful that adjustment turns out to be as the ecosystem explores this new terrain.