DEV Community

Cover image for React vs Signals: 10 Years Later
Ryan Carniato for This is Learning

Posted on

React vs Signals: 10 Years Later

How does the old Winston Churchill quote go?

Those who fail to learn from history are doomed to repeat it

Although a more ironic addendum might add.

Those who study history are doomed to stand by while everyone else repeats it.

In the past few weeks, we've seen the culmination of a build of excitement around the revival of fine-grained reactivity, being referred to as Signals, across the front-end world.

For a history of Signals in JavaScript check out my article:

The truth is Signals never went away. They lived several years in obscurity as third-party libraries or hidden behind frameworks' plain object APIs. Even though the common rhetoric that came in with React and the Virtual DOM condemned the patterns as unpredictable and dangerous. And they weren't wrong.

But there is more to it than a 10 year old debate. So I want to talk about how things have changed over the years and offer SolidJS as a foil.


"Fixing" Front-end

At the core of this conversation is understanding what React is. React is not its Virtual DOM. React is not JSX. To this date one of my explanations on the subject came from one of Dan Abramov's earliest articles You're Missing the Point of React, where he state React's true strengths are:

composition, unidirectional data flow, freedom from DSLs, explicit mutation, and static mental model.

React has a very powerful set of principles that guide it that are more important than any implementation detail. And even years later there is this notion from the thought leaders around React that they fixed front-end.

But hypothetically, what if they didn't? What if there were other ways to address the problems of the day that didn't involve such drastic re-alignment?


A Solid Alternative

The concept behind Solid is equally simple. It even shares ideas like composition, unidirectional data flow, and explicit mutation that made React feel like a simple answer to UI development. Where it differs is outside the world of reactivity everything is an Effect. It's almost the antithesis of React which treats everything you do as being pure (as in having no side effects).

When I think of creating a Counter button with Signals without any templating I'd do this:

function MyCounter() {
  const [count, setCount] = createSignal();

  const myButton = document.createElement("button");
  myButton.onclick = () => setCount(count() + 1);

  // update the text initially and whenever count changes
  createEffect(() => {
    myButton.textContent = count();
  });

  return myButton;
}
Enter fullscreen mode Exit fullscreen mode

I'd call my function and get a button back. If I need another button I'd do it again. This is very much set and forget. I created a DOM element and set up some event listeners. Like the DOM itself, I don't need to call anything for my button to update. It is independent. If I want a more ergonomic way of writing I use JSX.

function MyCounter() {
  const [count, setCount] = createSignal();

  return <button onClick={() => setCount(count() + 1)}>
    {count()}
  </button>
}
Enter fullscreen mode Exit fullscreen mode

Signals are not the same Signals as yesteryear. Their execution is glitch-free. They are push/pull hybrids that can model scheduled workflows like Suspense or Concurrent Rendering. And mitigate the leaky observer pattern with automated disposal. They have been leading benchmarks for several years not only for updates but for creation.


Immutability

So far so good? Well maybe not:

Apparently, this is something React solves. What did they solve exactly?

Putting Dan's specific concerns aside for a moment, it comes down to immutability. But not in the most direct way. Signals themselves are immutable. You can't modify their content and expect them to react.

const [list, setList] = createSignal([]);

createEffect(() => console.log(JSON.stringify(list())));

list().push("Doesn't trigger");

setList(() => [...list(), "Does trigger"]);
Enter fullscreen mode Exit fullscreen mode

Even with variants from Vue, Preact, or Qwik that use .value you are replacing the value not mutating it by assignment. So what does it mean that Signals are "mutable state"?

The benefit of having a granular event-driven architecture is to do isolated updates. In other words, mutate. In contrast, React's pure render model abstracts away the underlying mutable world re-creating its virtual representation with each run.

How important is this distinction when looking at two declarative libraries that drive updates off state if the data interfaces are explicit, side effects managed, and the execution well-defined?


Unidirectional Flow

I am not a fan of 2-way binding. Unidirectional Flow is a really good thing. I lived through the same things that are referenced in these tweets. You may have noticed Solid employs read/write segregation in its primitives. This is even true of its nested reactive proxies.

If you create a reactive primitive you get a read-only interface and a write interface. The opinion on this is so ingrained into Solid's design that members of the community like to troll me, abusing getters and setters to fake mutability.

One of the important things I wanted to do with Solid's design was to keep the locality of thinking. All the work in Solid is done where the effects are which is where we insert into the DOM. It doesn't matter if the parent uses a Signal or not. You author to your need. Treat every prop as reactive if you need it to be and access it where you need it. There is no global thinking necessary. No concerns with refactorability.

We re-enforce this by recommending when writing props you access the Signal's value rather than pass it down. Have your components expect values rather than Signals. Solid preserves reactivity by wrapping these in getters if it could be reactive.

<Greeting name={name()} />

// becomes
Greeting({ get name() {  return name() })

<Greeting name={"John"} />

// becomes
Greeting({ name: "John" })
Enter fullscreen mode Exit fullscreen mode

How does it know? A simple heuristic. If the expression contains a function call or property access, it wraps it. Reactive values in JavaScript have to be function calls so that we can track the reads. So any function call or property access, which could be a getter or proxy, could be reactive so we wrap it.

The positive is that for Greeting regardless of how you are consumed you access the property the same way: props.name. There is no isSignal check or overwrapping unnecessarily to make things into Signals. props.name is always a string. And being a value there is no expectation of mutation. Props are read-only and data flows one way.


Opt-In vs Opt-Out

This might be the crux of the discussion. There are a lot of ways to approach this. Most libraries have chosen reactivity for Developer Experience reasons because automatic dependency tracking means not needing to worry about missing updates.

It isn't hard to imagine for a React developer. Picture Hooks without the dependency arrays. The existence of the dependency arrays in Hooks suggests React can miss updates. Similarly, you opt into client components (use client) when using React Server Components. There are other solutions that have been automating this via compilation for years, but at times there is something to be said about being explicit.

It isn't generally a singular decision. You have things you opt into and things you opt out of in any framework. In reality, all frameworks are probably more like this:

A frameworks ideals can be beyond reproach but the reality is not so clear cut.

This brings me to this example:

These are 2 very different functions from Solid's perspective, because of Solid's JSX handling and the fact they only run once. This is not ambiguous and easily avoided once you are aware. And there is even a lint rule for that.

It's like expecting these to be the same:

const value = Date.now();
function getTime1() {
  return value;
}

function getTime2() {
  return Date.now();
}
Enter fullscreen mode Exit fullscreen mode

Moving the expression doesn't change what Date.now() does but hoisting changes the function's behavior.

Maybe it is less than ideal, but it isn't like this mental model isn't without its own benefits:


Can this be "Fixed" for real?

That's the logical follow-up. This is very much a language problem. What does fixed even look like? The challenge with compilers is that it is harder to account for edge cases and understand what happens when things go wrong. It is largely the reason historically React or Solid has been pretty careful about keeping clear boundaries.

Since the first introduction of Solid we've had people exploring different compilation because Signals as primitives are very adaptable and very performant.

In 2021, I took a stab at it.

React team also announced they were looking at this too.

There are rules being imposed on both systems. React wants you to remember not to do impure things in your function bodies. That is because if you do you can observe abstraction leaks if they were ever to optimize what is under the hood. Including potentially not re-running parts of your component.

Solid is already optimized without the need for the compiler or extra wrappers like React.memo, useCallback, useRef, but like React could benefit from some more streamlined ergonomics like not having to worry about indicating where Signals are read.

The end result is almost the same.


Final Thoughts

Image description

The oddest part of all of this is that the React team when looking at Reactivity doesn't feel like they are looking in the mirror. By adding Hooks they sacrificed part of their re-render purity for a model approaching that of Signals. And that by adding a compiler to remove the hooks concerned with memoization they complete that story.

Now it should be understood these were developed independently. Or atleast no acknowledgement was ever given which is not surprising given the sentiment of the old guard:

Fortunately React today is not the React of 10 years ago either.

React changed the front-end world by teaching us the important principles that should guide how to build UIs. They did so by strength of conviction being a unique voice of reason in a sea of chaos. We are where we are today due to their great work and countless have learned from their lessons.

Times have changed. It is almost fitting that the paradigm that was "fixed" away would resurface. It closes the loop. Completes the story. And when all the noise and tribalism fade what remains is a tale of healthy competition driving the web forward.


It is arguably never polite to construct a narrative by sewing together quotes out of context. Time moves on, and perspectives change. But this is a long and strongly held sentiment from React's thought leadership. One they've been vocal about since the beginning. I was able to grab all these quotes over conversations from just the last couple of days with very little effort.

For more on React's early history watch:

Latest comments (77)

Collapse
 
sentinelaeux profile image
sentinelae

Look at all the delusional people here, blinded by their own weaknesses, trying to use hype and corporate propaganda as a guide to their decisions. Dan Abramov is an authority with golden handcuff$, forced by himself to believe in the lie$ he was told. The very premise he uses to justify React is completely false and broken from the start. React is a problem sold as solution to dispute web dev market share, no more than this. Every attempt to fix the insanity that it brings is a failure from the start, as it is not really meant to solve anything. Whatever React does is to bring more confusion, more reinvention-of-the-wheel, more inefficiencies, more infinite pointless discussions to solve trivial problems that are not real problems outside React, and worse experiences overall. The whole "reactive" concept is pure propaganda, as it was never better than vanilla JS. The other frameworks are just as useless as React, they don't solve anything new, have the same problems, and keep shoving whatever the propaganda suceeds to sell to naive devs. If you really want to suceed in web dev, just realize how awful React is, then learn proper JS, CSS and HTML well, and you'll be able to take over the world. React/JSX/reactivity is complete bullshit.

Collapse
 
sylwesterdigital profile image
Sylwester Mielniczuk

As far as I know Facebook React is not web standard (W3C) and never will be. People devoted brain to this web nightmare are total lunatics.

Collapse
 
gabrielhangor profile image
Gabriel Khabibulin

Just use Vue ffs

Collapse
 
gaargh profile image
External • Edited

Dear @ryansolid,

How do you prevent transient "state" updates from being shown on the UI without a VDOM?

KR<

Collapse
 
ryansolid profile image
Ryan Carniato

The answer is scheduling. Even with a reactive system like Signals you schedule side effects. These systems are called "push/pull". As we render and update we push a dirty notification, this tells us what has possibly changed but we don't run any side effects until we know that its dependencies have changed. In the frame of the update we collect what could update and then afterwards we run those all together pulling out the latest values. Since we automatically track dependencies on each run we can trace the dependencies of the previous run to to know if they have in fact changed and then run the effect if necessary.

In so we get the benefit of both push based systems of not over executing things that are unrelated to the source of change, and pull based systems of not executing things that aren't currently in use (asked for).

Collapse
 
gaargh profile image
External

OK! Can you explain how this is implemented in Solid? I fail to see what's the purpose of a batch function if you provide a scheduler with such properties.

Thread Thread
 
gaargh profile image
External

@dan_abramov Your take?

Thread Thread
 
ryansolid profile image
Ryan Carniato

It was because Solid's current version batches synchronously which means we need to have the ability to wrap it so it knows to run at the end. Reactive execution (like reacting to a signals change) does this automatically but we don't currently wrap async entry points like events. This is a very rarely used API and even VDOM libraries like Inferno have the same immediate behavior in event handlers. We will autobatch into a microtask queue in these cases in the future (which is what Vue does), but it was a something I did early on imitating some of the fastest libraries.

Collapse
 
jakubkeller profile image
Jakub Keller

Unrelated.

Collapse
 
ionshard profile image
Victor Ling • Edited

This discussion is super interesting to me because I didn't learn React. I started out using Reagent which is a Clojurescript wrapper around React. Then when I eventually started using React itself I found it lacking. Now reading this discussion on Solid it looks like an attempt to do what Reagent did from the start but a little more wrong.

To start Reagent has the same concept as signals through Reactive Atoms (or ratoms for short) which you can dereference to get the current value and when you derefernce an ratom, the component you are rendering gets subscribed to that ratom for updates and will re-render anytime the value in the ratom changes.

However, one thing I think Reagent does right that both React and Solid seem to do wrong is in Reagent you can define a component as just a function that returns an element and this becomes the component's render function. This makes it work just like React currently, it gets called each time the component updates and rerenders the component from the output.

You can ALSO instead of defining a component straight from a render function. You can create a component from a function that returns a render function. This makes Reagent work the way that Solid seems to be proposing but the interface between initial render and subsequent renders is clear as day because it's using native language features you can clearly see the delineation between what is called when. You can do any initialization that should only run once in the outer function and then with a closure you can use that information inside the render function.

To use Dan Abramov's example in a mock Reagent way it would be thusly:

function VideoList({ videos, emptyHeading }) {
  let somethingElse = 42; // can add something here

  return function() {
    const count = videos.length; // Assuming accessing videos here creates an update subscription the way Solid does
    let heading = emptyHeading;

    if (count > 0) {
      const noun = count > 1 ? 'Videos' : 'Video';
      heading = count + ' ' + noun;
      somethingElse = someOtherStuff(); // can add something here too
    }
    return (
      <>
        <h1>{heading}</h1>
        <h2>{somethingElse}</h2>
      </>
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

Any access of data in the outer function only runs on initialization, any data in the inner function creates a subscription to re-run the inner render function only when the data changes. Instead of it needing a linter so you can catch where this invisible "this won't re-run" boundary is, it's clear as day via a function closure.

UPDATE: Looking into SolidJS more, it looks like the second example is exactly what it does but via "compiler magic" so I assume that is where the confusion seems to stem from. You need to know where the compiler magic happens rather than being explicit.

Collapse
 
inwerpsel profile image
Pieter Vincent • Edited

Fun fact: take a hook that uses a subscription (e.g. by directly using useSyncExternalStore), and use it as a component...

You now have a signal, written in React, with 0 external dependencies.

function useFoo() {
  return useSyncExternalStore( /* ... */);
}

function Normal() {
  const foo = useFoo();

  return foo === 'bar' ? BarComponent : OtherComponent;
}

// Temporary uppercase name to make JSX work.
const FooSignal = useFoo;
// Yes, this only needs to happen once for the whole app.
const $foo = <FooSignal />;

function WithSignals() {
  return <div>
    // ...
    <h2>{$foo}</h2>
    <p> Some other stuff that last rendered at {performance.now()}</p>
  </div>;
}
Enter fullscreen mode Exit fullscreen mode

This works with any argumentless hook that properly manages global state.

working codesandbox

Collapse
 
magicspon profile image
Dave Stockley • Edited

I get the feeling that react is so far up it's own arse that it can't admit that mistakes have been made. I say that as someone who loves using react. But me oh my, solid looks soo much nicer. Once the solid eco system has grown a bit, I will 100% be switching over.

Collapse
 
0okba3 profile image
Ook Baar • Edited

I'm quite new at web development, (less than 2 two years, no job experience yet); I come from an audio-visual design background among other things I've done, and I choose to continue my career path by expanding it to the interactivity that internet gives. And yes, it's powerful.

So at some point people who knows told me that React was the real thing. The big community support, and everything you already know by memory. So I began to study React. First thing I noticed: third party boilerplates: create react app, it was like everything was settle down to work with that because that Webpack thing was a pain if you wanted to configure it by yourself. I got it. A couple of month later I noticed that when I needed to use some packages it just didn't work, and my fellows learners had errors everywhere, of course we have Vite already to save us... Then I was told I needed to learn Class components because, while that was not the actual way React recommends to write code, there may be many companies that still use Class components etc. So I learnt Class Components, and Functional Components, and the Hooks stuff. But it seemed that the applications could run wild anyway if they got bigger and bigger, and I was told that for those cases Redux was useful. So while I was learning Redux and React-Redux suddenly things didn't worked. Again. Because it seemed there was a better way to write Redux now and it was Redux Toolkit and well, I went through Redux Toolkit. Some functionalities look kind of odd in relation with some extremely important principles that should never be broken because really important reasons support them -and best practices too-, like: don't mutate the state. Don't ever use the .push method (I heard that like several thousand times in a few months), but there was the push method again because of some thing that I don't really know, but somehow I could tell that it was abstracting everything at a level that someone who comes from zero probably won't actually ever know what's going on inside those functions and etc. It was cool anyway, declarative code is cool. Then, or in the meanwhile - it doesn't matter anymore- there was something wrong about rendering your app from the client side, so suddenly I was reading everywhere the server side rendering stuff. And Next.js. Well, as I was moving ahead (because when I began to learn web development there was JQuery -dying-, React (rules), Vue.js and Angular (there were probably others but you know what I mean), so as I was saying while I was learning React it seemed developers found the need to build some other frameworks too, Svelte (I was told is really good), there was something called Preact, and I didn't try it but it's name is funny (I can't wait for some other one, some Proto-React-Post-Preact and Wherever.js framework constructed over it, it will be really fun, yeah...).

So, signals are called? Yes, it sounds like something awesome that no real web developer who takes his job seriously should avoid. I'm sure Signals rules.

Well, now I realized that all that javascript that seemed so so scary the first days, makes me fell really really sure nowadays. It's weird, because I thought that the point of React was to make JavaScript easier to write. But I'm probably wrong because, as I said, I'm quite new at web development.

Collapse
 
chadsteele profile image
Chad Steele

SolidJS rocks... React, like Java, .NET, etc. is suffocating under it's own obesity. Yes, there's still flat earthers and React devotees. I'll never understand why people will choose dogma over liberty, but it happens over and over again.
sigh.
If you haven't already, try solid. If you run a company, switch. Now. It'll save you $

Collapse
 
dawoodkarim profile image
dawoodkarim

Thank you for the tips!

Collapse
 
beeplin profile image
Beep LIN

Image description

Image description

This confusion is mainly caused by the compile-time tricks, saving developer from writing () => repeatly. But personally I prefer more verbose while more "reasonable" way, that is, the original js way, with less compiler tricks:

function OneWrong(props) {
  const doubled = props.count * 2;
  return <div>Count: {() => doubled}</div> // wrong position for `() =>`
}

function OneRight(props) {
  const doubled = () => props.count * 2; // right position for `() =>`
  return <div>Count: {doubled}</div>
}

function Two(props) {
  return <div>Count: {() => props.count * 2}</div>
}
Enter fullscreen mode Exit fullscreen mode

So every javascript developer knows what's happening here. Basically all reactive holes in JSX should be lazy-evaluted functions.

Collapse
 
factordiceomar profile image
OmarC

So… what exactly is a “Signal”?

Collapse
 
ryansolid profile image
Ryan Carniato
Collapse
 
devvsakib profile image
Sakib Ahmed

++

Collapse
 
akmjenkins profile image
Adam • Edited

I think the principles that both frameworks espouse matter vastly more than either syntax. Everybody's standing on the shoulders of the giants before them.

I actually find it ridiculous and sad that the author of SolidJS wrote a flamewar article, and also that thought leadership of React engaged so deeply with it (and leading up to it, from looking at the tweets).

The best code is the simplest code. React code can be simple. SolidJS code can be simple, but neither of them can be simple in the same way, because they take fundamentally different approaches. That's fine.

They can also both be monstrously complicated (we all know, we've all been in the legacy project that's been stitched together and somehow works against all sensible rules of the framework). Neither framework can completely forbid devs from doing stupid things (though both do a great job of helping out where they can).

Give it a rest.