DEV Community

Cover image for Making the Case for Signals in JavaScript
Ryan Carniato for This is Learning

Posted on

Making the Case for Signals in JavaScript

Amidst all the conversation around Signals the recent weeks, I have forgotten to talk about arguably the most important topic. Why should you care? I've covered their evolution and opposition, but I haven't actually made the case for using them.

There has been a narrative around them related to performance which is not without merit but it is so much more than that. It is more than about developer experience too. It is about flipping the current paradigm on its head.

React famously popularized:

view = fn(state)

Which is a very powerful mental model for thinking about UIs. But more so it represents an ideal. Something to strive for.

Reality is a lot messier. The underlying DOM is persistent and mutable. Not only would naive re-rendering be prohibitively costly, but it would also fundamentally break the experience. (Input's losing focus, animations, etc...)

There are ways to mitigate this. We build ever-better constructs in hopes of reshaping our reality to fit. But at some point, we need to separate the implementation from the ideal to be able to talk about these things honestly.

So today we look at Signals as they are and what they have to offer.


Decoupling Performance from Code Organization

Image description

This is the moment when you first realize that something really different is going on. There is more to it, though. This is not an invitation to litter your app with global state, but rather a way to illustrate state is independent of components.

function Counter() {
  console.log("I log once");

  const [count, setCount] = createSignal(0);
  setInterval(() => setCount(count() + 1), 1000);

  return <div>{count()}</div>
}
Enter fullscreen mode Exit fullscreen mode

Similarly, a console.log that doesn't re-execute when a counter updates is a cute trick but doesn't tell the whole story.

The truth is this behavior persists throughout the whole component tree. State that originates in a parent component and is used in a child doesn't cause the parent or the child to re-run. Only the part of the DOM that depends on it. Prop drilling, Context API, or whatever it is the same thing.

And isn't just about the impact of spreading state changes across components but also multiple states within the same component.

function MoreRealisticComponent(props) {
  const [selected, setSelected] = createSignal(null);

  return (
    <div>
      <p>Selected {selected() ? selected().name : 'nothing'}</p>

      <ul>
        {props.items.map(item =>
          <li>
            <button onClick={() => setSelected(item)}>
              {item.name}
            </button>
          </li>
        )}
      </ul>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Porting the example from Svelte Creator Rich Harris' Virtual DOM is Pure Overhead to SolidJS it is important to understand, with Signals, updating the selected state does not cause any execution other than changing the text in that <p>. There is no re-running the list or diffing it. That is even true if the name in one of the rows updates. With Signals, we can directly update the text of that one button.

Note: It is customary in Solid to use <For> component instead of .map when rendering a loop so as to not recreate every row when entries of the items are inserted, removed, or moved.

You might be thinking, "I get it. It's fast. But I've never had trouble ensuring good performance in something like React." But the takeaway is more than that.

You no longer need to be concerned with components for optimal execution.

You can put your whole app in one component or many components and get the same benefit. You can break apart components for your sake. How you want to organize your app. Not because you need to isolate some part of the UI that changes. If performance ever became a concern it wouldn't be due to the structure of your components necessitating costly refactoring.

This is not an insignificant benefit to developer experience.


Separating Dynamic from Static

There have been some conversations to suggest this is a bad thing. If you want more perspective see @dan_abramov's response to my previous article.

I don't just want to talk about why this is a good thing, but how it is actually an amazing thing. Being able to optimize for each is beneficial. This is one of those places where aligning with the underlying platform pays off with dividends.

Classically speaking there is a tradeoff with using a distributed event system like Signals vs something that runs top-down. While updates will be quicker for the event system, at creation it has the additional overhead of setting up subscriptions.

This is even compounded by the fact that the web generally is a document-oriented interface. Even in Single Page Apps, you will be doing a lot of navigation which involves a lot of creation.

Makes sense. However, the web platform is aware of this cost and has made it more efficient to create elements in bulk than individually. Extracting the static parts for mass creation makes a bigger impact than those subscriptions.

And the benefits don't stop with the browser. With a Signals-based system the complexity, size, and execution of your code scale with how interactive your UI is rather than how many elements are in it.

Consider a server-rendered page with few interactions. Maybe an eCommerce site. The static parts are server-rendered HTML. You don't even need the code for that to make it interactive. Just delete the static parts from the bundle.

Steve from Builder.io (at 1:16) explains how this works in Qwik:

Admittedly this is mostly a performance concern. It comes from the same motivation for Islands architecture and React Server Components. It addresses a very real pain point we are facing today with the trend towards ever-bigger JavaScript bundles and slow initial page loads.

Overall, my position is this separation leads to a certain amount of transparency. It makes it easier to explain and reason about what is actually going on. While less simple than the ideal, it makes escape hatches, which are an important part of any system, more coherent.


Universalizing the Language of UI

One of the most powerful things about Signals is viewing their impact as a language. And I don't mean a compiled language. Signals are completely a runtime mechanism. There is no magic here. Just a Directed Acyclic Graph.

While it seems clear there is a convergence in concepts of State, Derived State, and Effects, not all the mental models and implementations line up.

Signals are independent of any component or rendering system. And only represent state relationships. Unlike something like React Hooks which have additional primitives to describe how to guard execution like useCallback, React.memo and concepts like stable references(useRef) to handle the coordination of effects.

Both of Dan's articles listed at the bottom of the article are a really good exploration into how to effectively use these primitives in React.

Additionally, Signals lend to traceability. They give you a way of understanding what updates and why.

They encourage patterns that lead to more declarative code. By making organizing code around data instead of component flow we can see what data is driving change. (Thanks Dan for the example).

// control flow
const count = videos.length;
let heading = emptyHeading;
let somethingElse = 42;
if (count > 0) {
  const noun = count > 1 ? 'Videos' : 'Video';
  heading = count + ' ' + noun;
  somethingElse = someOtherStuff();
}

// derived data
const format = (count) => count > 1 ? 'Videos' : 'Video';

const count = videos.length;
const heading = count > 0 ? format(count) : emptyHeading;
const somethingElse = count > 0 ? someOtherStuff : 42;
Enter fullscreen mode Exit fullscreen mode

It poses an interesting question about the purpose of code. Should we optimize it for making it easier to write or easier to read?


Ok, But What About the Tradeoffs?

Image description

There are definitely tradeoffs. The most obvious one is that they make the data special instead of the application of that data. We aren't dealing with plain objects anymore, but with primitives. This is very similar to Promises or Event Emitters. You are reasoning about the data flow rather than the control flow.

JavaScript is not a data flow language so it is possible to lose Reactivity. To be fair this is true of any JavaScript UI library or framework without the aid of tooling or compilation. For Signals, this is more emphasized as where you access the value is important.

I call this Signal's (singular) Hook Rule. There are consequences to this. There is a learning curve. It pushes you to write code a certain way. When using things like Proxies there are additional caveats like certain mechanisms in JavaScript language (like spreading, destructuring) have restricted usage.

Another consideration is around disposal. Subscriptions link both ways so if one side is long-lived it is possible to hold onto memory longer than desired. Modern frameworks are pretty good at handling this disposal automatically but this is inherent to Signal's design.

Finally, historically there were concerns about large uncontrollable graphs. Cycles and unpredictable propagation. These concerns largely are in past due to the work that has been done over the past several years. I'd go as far as these problems are what Signals solve and why you would use them over other message/event systems.


Conclusion

There are many ways to approach the challenge of creating great user interfaces. I've aimed at keeping this discussion grounded but I think there is a lot to get excited about here.

When you are building with a foundation of primitives there is a lot you can do. The exploration into reducing JavaScript load overhead and incremental interactivity is an area that Signals naturally fits in.

And thing is to use Signals to great benefit you do not need a compiler. Not even for templating. You can use Tagged Template Literals and do it all with no build step. We tend to use compilation though to make ergonomics smoother. But Signals are also a great choice for compilation.

Compilers and language exploration become that much easier when you have efficient building blocks you can target. And that isn't just true of us but for AIs. We've seen this suggested to improve upon everything from using analytics to drive code splitting to optimize initial load to optimizing compilers ability to understand code intent.

Whether Signals are best suited to be held by developers or to be low-level primitives for machines, they appear to be an important step in the ever-evolving world of web front-end.


Related Resources:

A Hands-on Introduction to Fine-grained Reactivity
The Evolution of Signals in JavaScript
React vs Signals: 10 Years Later
Virtual DOM is Pure Overhead By Rich Harris
Components are Pure Overhead
Metaphysics and JavaScript By Rich Harris
Making setInterval Declarative with React Hooks By Dan Abramov
Before you memo() By Dan Abramov

Latest comments (16)

 
peerreynders profile image
peerreynders • Edited

For components though, they are objects by their very nature (i.e HTMLElement) and so this is perfectly at home.

What needs to emphasized though is that Web Components aren't the components that consumers of component-based frameworks are looking for.

WC-technology isn't about application components, nor UI components but DOM components and require client side JavaScript to work; WCs are a product of the CSR -era.

Now there are people in the industry who recognize how to make WCs more PE and server friendly but when it comes to SSR it's usually something like:

We can accomplish both of these goals by employing a build step. And that’s what solutions like lit-ssr, WebC, and Enhance are helping to accomplish.

i.e. to "use the platform" fully you actually have to embrace non-standardized technologies (and there really is no hydration story).

Stepping back, WCs actually seem more of a sophisticated client side rendered alternative to iframe (About Web Components).

In a recent move

similarly SolidStart will be using the same Astro/Bling foundation for a server×client development platform (effectively forming the AST-Alliance).

So even if signals don't click for you, TanStack Start may be of interest to you (especially as Preact is supported from the get-go; they investigated progressive hydration back in 2019).

Perhaps something to think about for your "tinkering agenda".

 
leob profile image
leob • Edited

If that took you just 5 minutes then I think you're smarter than most people - I've always found JS's prototypal inheritance one of the more confusing concepts in programming languages - what's more, I don't really need "this" when programming JS, I'm perfectly able to get by without it, using objects only as "data objects", manipulating them with functions.

But yeah, different folks, different strokes.

(yes arrow functions do help, but then you need to use them consistently, I prefer to avoid "this" altogether)

P.S. I do like your idea about using "native" components (web components?) at the 'component' level, and using Vue (or React) components as the "glue" (pages/presenters) to tie them together ... the idea being that components (which contain no, or almost no, logic) will be reusable across frameworks, and you use the frameworks (Vue, React etc) at the application/"business logic" level - providing a more clear organization than everything being "component soup" :)

(I also see this as a more promising and practical approach than the efforts to use ONLY web components and NO frameworks)

 
leob profile image
leob • Edited

Saying goodbye to "this" is a big pro in my book, not a con ... "this" is, and always has been, hugely confusing in Javascript, with its prototype based inheritance and its other historical baggage / weirdness ... saying goodbye to it and switching to a functional paradigm is a blessing as far as I'm concerned. It would be different if Javascript were a real classical OOP language like Java or PHP or whatever - but it isn't.

Collapse
 
leob profile image
leob • Edited

The only thing that surprises me about the "signals hype" is that, in my mind, Vue has had this for ages, and they call it "reactive" or "refs" - ironically something ('reactivity') which React does NOT have :-D

This has, for me, always been the big advantage of Vue over React - reactiveness baked into the framework - hence the FRAMEWORK can optimize it, and the onus of preventing unnecessary re-rendering isn't put on the developer, as React does.

Are "signals" fundamentally different then from something Vue has had for ages? Vue is underrated, it should be way more popular than React.

Collapse
 
ryansolid profile image
Ryan Carniato

No. Vue hid away their reactivity early on which is why they don't get the nod but they were there. The reason the hype has been coming recently though is the return of finer grained rendering. So dropping the VDOM (which both React and Vue use) and using reactivity to manage rendering. Vue is building their own version inspired by SolidJS, called Vapor.

Collapse
 
leob profile image
leob

Thanks for mentioning Vapor, I missed that one!

(so you can tell I haven't actually used or followed Vue for some time - right now using ... React! Lol, for practical reasons that are more or less beyond my control)

That's great news to hear, speaks volumes about the flexibility of Vue's architecture.

Thread Thread
 
ryansolid profile image
Ryan Carniato

Right, this is the power of Signals. It gives you all the glue you need to model any number of things efficiently and cohesively.

 
peerreynders profile image
peerreynders • Edited

Rich Harris the author of Svelte, made the claim that “Frameworks aren’t there to organize your code, but to organize your mind”. I feel this way about Components. There are always going to be boundaries and modules to package up to keep code encapsulated and re-usable. Those boundaries are always going to have a cost, so I believe we are best served to optimize for those boundaries rather than introducing our own.

The Real Cost of UI Components (2019)

In SolidJS components exist for DX but aren't some kind of fundamental building block.

Collapse
 
appurist profile image
Paul / Appurist

It poses an interesting question about the purpose of code. Should we optimize it for making it easier to write or easier to read?

I've been a professional developer since 1984, focusing on web development for the last 15 years. One of the overriding rules I've learned in 4 decades is that maintainability and readability are by far more important than most other factors, including performance. Readability and maintainability lead to more understandable code, which tends to be more reliable (less buggy), and more performant naturally, since the developer(s) can more easily see where poor performance may come from.

So for me, it's a question of do you optimize for "the 10% case" or "the 90% case". Code tends to be (WORM-like) written once and read many times and while there may be updates, for each subsequent change, the user is again reading it and trying to understand it in order to make a change. Very little code is fire-and-forget, or in this case, write-only (and just run repeatedly without reading it again). Even if it is, there are two access points for us humans, writing it and reading it and we're much more likely to need to do the second repeatedly than the first. (This is subjective but I believe it is accurate enough for this discussion.)

Maintainability is the critical decision, which means readability is. So there's no doubt in my mind that ease of writing is less important than ease of reading. Writing involves reading anyway. You look at that code you just wrote and try to determine if it makes sense, if it's good, ready to ship. You do that by rereading the code after writing it. If it's readable, it's more likely to be understandable and all of that happens while writing it too. It's less likely to have a bug or miss the major purpose for writing it in the first place.

The "both" answers are just avoiding your question. Yes both are important but reading is more important, and the longer the code survives, the truer that becomes.

So I'm all-in on optimizing for reading.

Collapse
 
arnebab profile image
Arne Babenhauserheide

Is there a difference between these and signals and slots in Qt?

Collapse
 
chrisczopp profile image
chris-czopp

To me, no magic in signal's internals means no magic when using the mechanism. That ultimately means simplicity and predictability which is something we all strive for.

 
srav001 profile image
Sravan Suresh

Although I agree that a setter is less ergonomic to use, it has nothing to do with signals.

Vue also uses signals. It is only a name for a type of reactivity.

Solid calls the primitives signals itself whereas Vue calls it ref/reactive.

Thread Thread
 
peerreynders profile image
peerreynders • Edited

The absence of this is precisely what tells me a given component-based framework’s design is fundamentally flawed. And I say that as no faithful adherent to any particular programming paradigm.

(1) Prevalence of this typically happens when methods are used rather than functions. In turn it happens when JS Objects are used in the role of "objects" rather than records (i.e. as associative arrays). So they tend to surface more with OO style code. There is nothing wrong with classes, they have their uses especially since the introduction of custom elements.

In SolidJS libraries this is typically used in the role of the function context.

For example in SolidStart I used this:

function userFromSession(this: ServerFunctionEvent) {
  return userFromFetchEvent(this);
}
Enter fullscreen mode Exit fullscreen mode

(2) SolidJS is not a component-based framework. SolidJS is a state library that happens to render. You can author in a component style if that is what you want to do.

(3) What gets drowned out in the signals discussion is that there are also reactive stores.

In my SolidStart TodoMVC demo I use signals to augment server side data with client side state but then reconcile the result into a store before projecting it onto the DOM in order to minimize DOM updates:

function makeTodoItemSupport(
  filtername: Accessor<Filtername>,
  todos: Accessor<TodoView[]>
) {
  const [todoItems, setTodoItems] = createStore<TodoView[]>([]);

  const counts = createMemo(() => {
    /* … more code … */

    filtered.sort(byCreatedAtDesc);
    setTodoItems(reconcile(filtered, { key: 'id', merge: false }));

    return {
      total,
      active: total - complete,
      complete,
      visible,
    };
  });

  return {
    counts,
    todoItems,
  };
}
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
deliastruble profile image
DeliaStruble

This article provides a compelling argument for adopting signals and enhances our understanding of modern web development. Great read! cricketbuzz.com register

Collapse
 
brucou profile image
brucou • Edited

I see where you are coming from. Syntax does matter. For this reason, some folks prefer JSDoc comments to TypeScript even when TypeScript delivers better typing. For the same reason, we build new languages or add new constructs to existing ones. Programming is reading, thinking, and writing. A programming language should support that. I actually posit that programming is thinking first, and reading and writing second. So being able to express concepts directly as you think them helps a lot minimizing that discrepancy between what you imagined you wrote and what you actually did (where bugs lie).

With that said, I don't see signals entering the JS language any time soon. One key issue being that there is no agreement in their semantics, which is the first thing that must happen before standardizing on syntax. So the alternative is going to be what we have today, compilers, preprocessors, or JS-like languages -- all with similar but different semantics and widely varying syntax/API. I think Svelte got the balance right (see my point about JSDoc comments vs. TypeScript) with making a new language that complects JS/HTML/CSS and reactivity with just enough new pieces to make it quickly learnable.

Collapse
 
ryansolid profile image
Ryan Carniato

Are you adverse to calling functions? Are you adverse to awaiting promises? Did .then make you miserable? We have what we have in terms of available syntax. I can understand if compilers can ease this for sure and likely will. But we've had all sorts of syntaxes over the years. Is it fun to prefix things with this.?

If Signals were new I might have similar reservation I suppose. But when I think of the realm of all possible syntaxes I've used over the years from node callbacks, to psuedo classes in the early frameworks its hard to see what you are getting at.

In any case that has been the norm for some number of developers for years and it isn't really different than anything else. I suppose if it bugs you that much don't partake but I'm not really seeing it.