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!

    Frequently Asked Questions (FAQs) about Signals in JavaScript

    What are the key differences between Signals and traditional event handling in JavaScript?

    Traditional event handling in JavaScript involves attaching event listeners to DOM elements and responding to user interactions or system events. However, this approach can become complex and difficult to manage in large applications. Signals, on the other hand, offer a more fine-grained approach to reactivity. They allow you to create independent units of reactive behavior that can be composed together to create complex interactions. Unlike event listeners, signals are not tied to any specific DOM element or event type, making them more flexible and easier to reuse across different parts of your application.

    How does the performance of Signals compare to other reactivity models in JavaScript?

    Signals are designed to be highly efficient and performant. They use a pull-based reactivity model, which means they only compute their values when needed. This can lead to significant performance improvements in applications with complex reactive behavior, as unnecessary computations can be avoided. However, the exact performance difference will depend on the specific use case and how well the reactivity model is optimized.

    Can Signals be used with other JavaScript frameworks like React or Vue?

    Yes, Signals can be used with other JavaScript frameworks like React or Vue. They can be integrated into the reactivity systems of these frameworks to provide additional flexibility and control over reactive behavior. However, it’s important to note that each framework has its own way of handling reactivity, so you’ll need to understand how Signals can fit into that model.

    How do I debug a JavaScript application that uses Signals?

    Debugging a JavaScript application that uses Signals can be a bit different from debugging a traditional event-driven application. Since Signals are not tied to specific events or DOM elements, you’ll need to track the flow of data through your signals and their dependencies. Tools like the Chrome DevTools can be helpful for this, as they allow you to step through your code and inspect the state of your signals at each step.

    Are there any limitations or drawbacks to using Signals in JavaScript?

    While Signals offer many benefits, they also have some limitations. One potential drawback is that they can make your code more complex and harder to understand, especially for developers who are not familiar with the concept of reactivity. Additionally, because Signals are a relatively new concept in JavaScript, there may be fewer resources and community support available compared to more established patterns and frameworks.

    How can I test a JavaScript application that uses Signals?

    Testing a JavaScript application that uses Signals can be done using the same techniques as for any other JavaScript application. You can use unit tests to test individual signals and their behavior, and integration tests to test how signals interact with each other and with other parts of your application. Tools like Jest or Mocha can be used for this purpose.

    Can Signals be used in a Node.js environment?

    Yes, Signals can be used in a Node.js environment. They are not tied to the browser or the DOM, so they can be used in any JavaScript environment. This makes them a powerful tool for building reactive behavior in server-side applications as well.

    How do I handle errors in a JavaScript application that uses Signals?

    Error handling in a JavaScript application that uses Signals can be done in a similar way to any other JavaScript application. You can use try/catch blocks to catch errors and handle them appropriately. Additionally, you can use error signals to propagate errors through your signal graph and handle them in a centralized manner.

    Can Signals be used to build real-time applications?

    Yes, Signals can be used to build real-time applications. They provide a powerful way to manage and respond to real-time data updates, making them a great choice for applications like chat apps, live dashboards, and multiplayer games.

    How do I optimize the performance of a JavaScript application that uses Signals?

    Optimizing the performance of a JavaScript application that uses Signals involves carefully managing your signal dependencies and avoiding unnecessary computations. You can use tools like the Chrome DevTools to profile your application and identify performance bottlenecks. Additionally, you can use techniques like memoization and lazy evaluation to improve the performance of your signals.