Signals: Fine-grained Reactivity for JavaScript Frameworks

    Share

    In this article, we’ll dive into how to use signals in Solid, a modern, reactive JavaScript library for building user interfaces that primarily rely on components.

    Contents:

    1. An Introduction to Signals
    2. What is Solid?
    3. What Exactly Is a Signal?
    4. A Signals Example
    5. Signals in Angular
    6. Other Features of Solid

    An Introduction to Signals

    One of the latest trends in web development is the use of signals, which offer a more reactive approach to updating values in a program that are subject to change. When a value is updated, everything that uses that value is also updated. This is what makes signals so unique.

    The growth of signals, and the interest in them, is reminiscent of all the commotion that greeted version 16.8 of React in 2019, when the React team introduced hooks. The aim of hooks was to make state updates (and eventually all updates) more functional in approach, and to move away from using classes. Whilst signals seem almost the same as hooks, there are some subtle differences that set them apart (which we explore below).

    Where signals are being used

    What is Solid?

    Solid (also known as SolidJS) was created by Ryan Carniato in 2016 and released in 2018. In his own words, it “came out of the desire to continue to use the fine-grained reactive patterns I’d come to love from Knockout.js.”

    He wasn’t a fan of the direction that libraries like React and Vue were taking at the time, and “just preferred the control and composability that comes with using primitives smaller than, and independent from, components.” His solution was to create Solid, a reactive framework that uses signals to create fine-grained reactivity — a pattern for signals that can now also be seen in many other frameworks.

    At first glance, Solid looks a lot like React with hooks and functional components. And in some ways, that’s right: they both share the same philosophy when it comes to managing data, which makes learning Solid much easier if we’re already familiar with React.

    But there are a few key differences:

    • Solid is pre-compiled, in a similar way to Svelte. This means that the performance gains are baked into the final build, and therefore less code needs shipping.
    • Solid doesn’t use a virtual DOM, and even though components are just functions, like in React, they’re only ever called once when they’re first rendered. (In React, they’re called any time there’s an update to that component.)

    What Exactly Is a Signal?

    Signals are based on the observer pattern, which is one of the classic Gang of Four design patterns. In fact, Knockout uses something very much like signals, known as “observables”.

    Signals are the most atomic part of a reactive application. They’re observable values that have an initial value and provide getter and setter methods that can be used to see or update this value respectively. However, to make the most out of signals, we need reactions, which are effects that subscribe to the signal and run in response to the value changing.

    When the value of a signal changes, it effectively emits an event (or “signal”) that then triggers a reaction (or “effect”). This is often a call to update and render any components that depend on this value. These components are said to subscribe to the signal. This means that only these components will be updated if the value of the signal changes.

    A key concept in Solid is that everything is an effect, even view renders. Each signal is very tightly tied to the specific components that it affects. This means that, when a value changes, the view can be re-rendered in a very fine-grained way without expensive, full page re-renders.

    A Signals Example

    To create a signal in Solid, we need to use the createSignal function and assign two variables to its return value, like so:

    const [name, setName] = createSignal("Diana Prince");
    

    These two variables represent a getter and setter method. In the example above, name is the getter and setName is the setter. The value of 0 that’s passed to createSignal represents the initial value of the signal.

    For React developers, this will of course look familiar. The code for creating something similar using React hooks can be seen below:

    const [name, setName] = useState("Diana Prince");
    

    In React, the getter (name) behaves like a variable and the setter (setName) is a function.

    But even though they look very similar, the main difference is that name behaves like a variable in React, whereas it’s a function in Solid.

    Having name as a function means that, when called inside an effect, it automatically subscribes that effect to the signal. And this means that, when the value of the signal changes, the effect will run using the new value.

    Here’s an example with our name() signal:

    createEffect(() => console.log(`Hello ${name()}`))
    

    The createEffect function can be used to run effects that are based on the value of any signals, such as logging a value to the console. It will subscribe to any signals that are referenced inside the function. If any values of the signal change, the effect code will be run again.

    In our example, if we change the value of the name signal using the setName setter function, we can see the effect code runs and the new name is logged to the console:

    setName("Wonder Woman")
    

    Having the getter as a function also means the most up-to-date current value is always returned, whereas other frameworks can often return a “stale” value even after it’s been updated. It also means that any signals can easily be bounded to calculated values and memoized:

    const nameLength = createMemo(() => name().length)
    

    This creates a read-only signal that can be accessed using nameLength(). Its value updates in response to any changes to the value of the name signal.

    If the name() signal is included in a component, the component will automatically subscribe to this signal and be re-rendered when its value changes:

    import { render } from "solid-js/web"
    import { createSignal } from "solid-js"
    
    const HelloComponent = () => {
      const [name, setName] = createSignal("Diana Prince");
      return <h1>Hello {name()}</h1>
    }
    
    render(() => <HelloComponent />, document.getElementById("app"));
    

    Updating the value of the name signal using setName will result in the HelloComponent being re-rendered. Furthermore, the HelloComponent function only gets called once in order to create the relevant HTML. Once it’s been called, it never has to run again, even if there are any updates to the value of the name signal. However, in React, component functions get called any time a value they contain changes.

    Another major difference with Solid is that, despite using JSX for its view logic, it doesn’t use a virtual DOM at all. Instead, it compiles the code in advance using the modern Vite build tool. This means that much less JavaScript needs to be shipped, and it doesn’t need the actual Solid library to ship with it (very much in the same way as Svelte). The view is built in HTML. Then, fine-grained updates are made on the fly — using a system of template literals to identify any changes and then perform good old-fashioned DOM manipulation.

    These isolated and fine-grained updates to the specific areas of the DOM are very different from React’s approach of completely rebuilding the virtual DOM after any change. Making updates directly to the DOM reduces the overhead of maintaining a virtual DOM and makes it exceptionally quick. In fact, Solid has some pretty impressive stats regarding render speed — coming second only to vanilla JavaScript.

    All of the benchmarks can be viewed here.

    Signals in Angular

    As mentioned earlier, Angular has recently adopted signals for making fine-grained updates. They work in a similar way to signals in Solid, but are created in a slightly different way.

    To create a signal, the signal function is used and the initial value passes as an argument:

    const name = signal("Diana Prince")
    

    The variable name that the signal was assigned to (name in the example above) can then be used as the getter:

    console.log(name)
    << Diana Prince
    

    The signal also has a set method that can be used to update its value, like so:

    name.set("Wonder Woman")
    console.log(name)
    << Wonder Woman
    

    The fine-grained approach to updates in Angular is almost identical to the approach in Solid. Firstly, Angular has an update() method that works similarly to set, but derives the value instead of replacing it:

    name.update(name => name.toUpperCase())
    

    The only difference here is taking the value (name) as a parameter and performing an instruction on it (.toUpperCase()). This is very useful when the final value that the getter is being replaced with isn’t known, and must therefore be derived.

    Secondly, Angular also has the computed() function for creating a memoizing signal. It works in exactly the same way as Solid’s createMemo:

    const nameLength = computed(() => name().length) 
    

    Much like with Solid, whenever the value of a signal in the calculation function is detected to have changed, the value of the computed signal will also change.

    Finally, Angular has the effect() function, which works exactly like the createEffect() function in Solid. The side effect is re-executed whenever a value it depends on is updated:

    effect(() => console.log(`Hello`, name()))
    
    name.update(name => name.toUpperCase())
    // Hello DIANA PRINCE
    

    Other Features of Solid

    It’s not just signals that make Solid worth looking at. As we’ve already noted, it’s blazingly fast at both creating and updating content. It also has a very similar API to React, so it should be very easy to pick up for anyone who’s used React before. However, Solid works in a very different way underneath the hood, and is usually more performant.

    Another nice feature of Solid is that it adds a few nifty touches to JSX, such as control flow. It lets us create for loops using the <For> component, and we can contain errors inside components using <ErrorBoundary>.

    Additionally, the <Portal> component is also handy for displaying content outside the usual flow (such as modals). And nested reactivity means that any changes to an array of values or an object will re-render the parts of the view that have changed, rather than having to re-render the whole list. Solid makes this even easier to achieve using Stores. Solid also supports server-side rendering, hydration and streaming out of the box.

    For anyone keen to give Solid a try, there’s an excellent introductory tutorial on the Solid website, and we can experiment with code in the Solid playground.

    Conclusion

    In this article, we’ve introduced the concept of signals and how they’re used in Solid and Angular. We’ve also looked at how they help Solid perform fine-grained updates to the DOM without the need for a virtual DOM. A number of frameworks are now adopting the signal paradigm, so they’re definitely a trick worth having up our sleeve!