Learn why Gartner just named Builder a Cool Vendor

Announcing Visual Copilot - Figma to production in half the time

Builder.io logo
Contact Sales
Contact Sales

Blog

Home

Resources

Blog

Forum

Github

Login

Signup

×

Visual CMS

Drag-and-drop visual editor and headless CMS for any tech stack

Theme Studio for Shopify

Build and optimize your Shopify-hosted storefront, no coding required

Resources

Blog

Get StartedLogin

‹ Back to blog

Web development

useSignal() is the Future of Web Frameworks

February 16, 2023

Written By Miško Hevery

A signal is a way to store the state of your application, similar to useState() in React. But there are some key differences that give Signals the edge.

What Signal is

an image of the difference between useState and useSignal. useState ⇒ value + setter, useSignal ⇒ getter + setter

The key difference between Signals and State is that Signals return a getter and a setter, whereas non-reactive systems return a value (and a setter). Note: some reactive systems return a getter/setter together, and some as two separate references, but the idea is the same.

The issue is that the word State conflates two separate concepts.

  • StateReference: The state-reference is a reference to the state.
  • StateValue: This is the actual value stored in state reference/storage.

Why is returning a getter better than returning a value? Because by returning the getter, you can separate the passing of the state-reference from a reading of the state-value.

Let’s look at this SolidJS code as an example.

Screenshot of a code example of a counter component in Solid.js.
  • createSignal(): allocates the StateStorage and initializes it to 0.
  • getCount: a reference to the store that you can pass around.
  • getCount(): says retrieve the state value.

The above explains how Signals are different from the good old state but does not explain why we should care.

Signals are reactive! This means that they need to keep track of who is interested in the state (subscriptions) and, if the state changes, notify the subscribers of the state change.

To be reactive, Signals must collect who is interested in the Signal’s value. They gain this information by observing in what context the state-getter is invoked. By retrieving the value from the getter, you are telling the signal that this location is interested in the value. If the value changes, this location needs to be re-evaluated. In other words, invoking the getter creates a subscription.

This is why passing the state-getter rather than the state-value is important. The passing of state-value does not give the signal any information about where the value is actually used. This is why distinguishing between state-reference and state-value is so important in signals.

For comparison, here is the same example in Qwik. Notice that (getter/setter) has been replaced with a single object with a .value property (which represents the getter/setter). While the syntax is different, the inner workings remain the same.

Screenshot of a code example of a counter component in Qwik.

Importantly, when the button is clicked and the value is incremented, the framework only needs to update the text node from 0 to 1. It can do that because, during the initial rendering of the template, the Signal has learned the count.value has been accessed by the text node only. Therefore it knows that if the value of the count changes, it only needs to update the text node and nothing else.

Let’s look at how React uses useState() and its shortcomings.

Screenshot of a code example of a counter component in React.

React useState() returns a state-value. This means that useState() has no idea how the state-value is used inside the component or the application. The implication is that once you notify React of state change through a call to setCount(), React has no idea which part of the page has changed and therefore must re-render the whole component. This is computationally expensive.

React has useRef(), which is similar to useSignal(), but it does not cause the UI to re-render. This example looks very similar to useSignal() but it will not work.

Screenshot of a code example of a counter component with useRef in React.

useRef() is used exactly like a useSignal() to pass a reference to the state rather than the state itself. What useRef() lacks are subscription tracking and notifications.

The nice thing is that in signal-based frameworks, useSignal() and useRef() are the same thing. useSignal() can do what useRef() does plus subscription tracking. This further simplifies the API surface of the framework.

Signals rarely require memoization because they do the least amount of work out of the box.

Consider this example of two counters and two children components.

Screenshot of a code example of a counter component with a child Display component in Qwik.

In the above example, only the text node of one of the two Display components will be updated. The text node that doesn't get updated will never print after the initial render.

# Initial render output
<Counter/>
<Display count={0}/>
<Display count={0}/>

# Subsequent render on click
(blank)

You actually can’t achieve the same in React because, at the very least, at least one component needs to re-render. So let’s look at how to memoize components in React to minimize the amount of re-rendering.

Screenshot of a code example of a counter component with a child Display component with the memo function wrapping it in React.

But even with memoization, React will rerun the re-render much more.

# Initial render output
<Counter/>
<Display count={0}/>
<Display count={0}/>

# Subsequent render on click
<Counter/>
<Display count={1}/>

Without memoization, we would see:

# Initial render output
<Counter/>
<Display count={0}/>
<Display count={0}/>

# Subsequent render on click
<Counter/>
<Display count={1}/>
<Display count={0}/>

That is a lot more work than what Signals have to do. So, this is why signals work as if you memoized everything without actually having to memoize anything yourself.

Let’s take a common example of implementing a shopping cart.

a screenshot of three components in React that make up the code for a shopping cart.

The state of the cart is usually pulled up to the highest common parent between the buy button and where the cart is rendered. Because the buy button and the cart are far apart in the DOM, this often is very close to the top of the component render tree. In our case, we call it the common ancestor component.

The common ancestor component has two branches:

  1. One which drills the setCart functions through many layers of components until it reaches the buy button.
  2. The other drills the cart state through many layers of components until it reaches the component which renders the cart.

The problem is that every time you click the buy button, most of the component tree has to rerender. This leads to an output similar to this:

# "buy" button clicked
<App/>
<Main/>
<Product/>
<NavBar/>
<Cart/>

If you do use memoization then you can avoid the setCart prop-drilling branch but not the cart prop-drilling branch, so the output would still look like so:

# "buy" button clicked
<App/>
<NavBar/>
<Cart/>

With signals, the output is like so:

# "buy" button clicked
<Cart/>

This greatly reduces the amount of code that needs to execute.

An image with 4 logos of frameworks that use signals.

Some of the more popular frameworks which support signals are Vue, Preact, Solid, and Qwik.

Now, signals are not new; they have existed in Knockout and probably other frameworks before then. What is different is that signals have greatly improved their DX in recent years through clever compiler tricks and deep integration with JSX, which makes them very succinct and a pleasure to use — and that part is genuinely new.

A signal is a way to store state in an application, similar to useState() in React. However, the key difference is that signals return a getter and a setter, whereas non-reactive systems return only a value and a setter.

It is important because signals are reactive, meaning they need to keep track of who is interested in the state and notify subscribers of state changes. This is achieved by observing the context in which the state-getter is invoked, which creates a subscription.

In contrast, useState() in React returns only the state-value, meaning it has no idea how the state-value is used and must re-render the whole component tree in response to state changes.

In recent years signals have reached a DX which makes them no harder to use than traditional systems. For this reason, I think the next framework you will use will be reactive and based on signals.

Introducing Visual Copilot: convert Figma designs to high quality code in a single click.

Try Visual CopilotGet a demo

Share

Twitter
LinkedIn
Facebook
Hand written text that says "A drag and drop headless CMS?"

Introducing Visual Copilot:

A new AI model to turn Figma designs to high quality code using your components.

Try Visual CopilotGet a demo
Newsletter

Like our content?

Join Our Newsletter

Continue Reading
ai8 MIN
The Big Lie AI Vendors Keep Telling You
November 27, 2024
AI8 MIN
Generate Figma Designs with AI
November 25, 2024
Design to code5 MIN
Builder.io Named a Cool Vendor in the 2024 Gartner® Cool Vendors™ in Software Engineering: User Experience
November 21, 2024