DEV Community

Discussion on: React vs Signals: 10 Years Later

Collapse
 
brucou profile image
brucou • Edited

I feel like a lot of people do not really accurately get this signals thing at a conceptual level. Let me allow myself to make an attempt at explaining how I see things in case others may find it helpful. I am going to use a basic DSL with only three constructs:

  1. event -> reaction (reaction)
  2. variable <- expression (assignment)
  3. variable = expression (abstraction)

Taking your simple counter example, in the end, the specs we are implementing are those:

route change -> (render counter at 0, count <- 0)
button click -> (count <- count + 1, render counter at count)
Enter fullscreen mode Exit fullscreen mode

In this simple example, there is no need to use syntax 3 but you could imagine something where doubleCount = 2 * count for instance.

Here:

  • render counter at 0 is document.createElement... etc.
  • render counter at count is button.textContent = count, etc

The specifications for a reactive system, category to which web apps belong, are the set of events that the app must react to, mapped to the corresponding reactions. As such syntax 1 is necessary. Because applications are stateful in general (i.e., the same event can trigger different reactions), syntax 2. is necessary to express the state of the application on which the reaction depend. Syntax 3 is optional (thanks to referential equality you could inline the variable everywhere) but is nice to have. I call it abstraction because the details (the expression) are abstracted behind the variable name. This is essentially the same as lambda abstraction.

These are the specs of the counter and they are independent from any implementation or framework or else.

Moving to the implementation realm, you need to find ways to implement event binding, do assignments, and perform effects (here just rendering). You can do that manually like a boss, or you can do through a library, or you can do it through a framework. As long as somehow you arrive to implement render count with sth of equivalent effect to button.textContent = count, your implementation will be correct. Whether your framework/library/brain arrives at that conclusion through diffing vDOM nodes, or through reactive proxies, or because you wrote it directly this way, it is all the same. Whether you used mutable state, proxies, or went to the moon and back to reach that conclusion is not relevant.

Except that it is. But not to the functional requirements of the application. It is relevant because the accidental complexity attached to the solution that you apply impacts the non-functional requirements (performance, efficiency, maintainability, etc.) and constraints ( time, people skills, budget etc.) of your project. Architects will recognize this pattern of thinking as they commonly understand an application as a set of those three things (functional reqs, non-functional reqs, and constraints).

VanillaJS works, but it can get tiring and error prone on large application to create and update all these nodes by hand. And that's just the first write. Maintain and evolve that is also going be a challenge. You can still write large apps in JS, but you are going to need solid design and modularization skills (or/and a good UI component library). It is not all impossible and once you have your patterns straight, it is refreshingly simple. (btw, example of vanillaJS app from Adobe leonardocolor.io/#)

Nonetheless, you may also realize that writing the code for the rendering work is repetitive and you may prefer instead to let the computer figure out for you the DOM updates to perform. vDOM diffing is a way to do that. Template languages (.vue, .svelte, etc.) are another way. Template literals yet another one and there are still more ways, as we know.

All those solve the problem of you having to write a ton of DOM update codes manually. As we discussed accidental complexity, while they solve the repetitiveness and boilerplate problem, they may also create others. So Angular 1 default change detection was to check everything for change after the occurrence of each event. That works but it can get really slow, as we observed. React's default is to rerender everything (I mean the whole component being rendered) and then do a diff with its previous version. That absolutely works but has also some inefficiencies attached to it. Template languages allow mapping easily a source of change to a DOM update operation. But then those languages are not good at expressing arbitrarily changing templates.

There would be more to say about the modularization story of each pattern. I want better to emphasize that the bad memories that folks have of two-way data binding may relate to two-way data binding being done wrong. Two-way data binding wants to implement sth like x = y with semantics like (x updates -> update y, y updates-> update x). There is an obvious circularity here that has caused problems in many implementations. Unidirectional dataflow avoid the issues of having to implement the 2-way pattern without falling into the traps by removing the circularity from the get-go. With that said, Vue and Svelte both have two-way data binding that works like a charm.

Oh I almost forgot to explain signals:

1. event    -> reaction         (reaction)
2. variable <- expression    (assignment)
3. variable = expression     (abstraction)
Enter fullscreen mode Exit fullscreen mode
  1. is createEffect (in Solid)
  2. is, well the setter/getter parts (setCount, count() in Solid)
  3. is for derived state (createMemo in Solid)