I spoke about how I think signals are the future of web frameworks. Since then, I am excited to welcome Angular to the Signals club.
What took me by surprise is this tweet where Andrew Clark wrote that he thinks that React Forget is a better approach than Signals.
React Forget is a labs project that aims to improve the rendering performance of React applications by automatically generating the equivalent of useMemo
and useCallback
calls to minimize the cost of re-rendering while retaining React’s programming model.
Signals don’t need memoization for the most part. React Forget automatically inserts memoization everywhere, so I can see how one could come out with the impression that memoization is the same as signals.
Let me show you where the memoization approach fails and Signals shine.
Let’s build a classic single-page application containing a buy button and a shopping cart. This example was chosen because it demonstrates a real-world scenario where state, mutation, and rendering are separated into distinct components:
- Where the state is declared (shopping cart content)
- Where the state is mutated (the buy button)
- Where the state is rendered (the shopping cart UI)
The key thing to understand is that we need to get the state (and the setter) down to the components that need it.
In our case, it is the ShoppingCart
and BuyButton
components. That means we must prop-drill our way through many layers of components to get there (context is an alternative way to do this, but the result is the same).
Now let’s look at what happens when we try to mutate the shopping cart:
- The
BuyButton
has thesetCart
setter, which is invoked with the new state of the cart. - The invocation of
setCart
invalidates theApp
component. - React starts re-rendering starting at the
App
component. Usually, this would result in most child components re-rendering, but let’s assume React Forget has turned memoization to the max. In that case, React still needs to render all components betweenApp
andShoppingCart
. In our simple case, it is justHeader
andUser
, but it is many more in a real-world app.
Even though React Forget compiler memoizes everything, it still could not avoid the fact that when the state mutates, it has to be prop-drilled from the state storage all the way down to the place where it is needed.
Re-rendering intermediate components that are used only for prop drilling is wasteful.
Now let’s look at signals. The data-flow diagram looks identical as before, except that instead of passing state, we are now passing signals through many layers of components to ShopingCart
and BuyButton
.
Now let’s look at what happens when we try to mutate the shopping cart.
- The
BuyButton
has asetCart
signal, which it invokes with the new state of the cart. - The signal notifies the
ShoppingCart
to update the state of the cart.
That is it.
Notice that even though the state is declared in the App
component, the App
is not part of the re-rendering process, and neither are all of the components which were part of the prop-drilling of the state to pass it to the ShoppingCart
.
That is where the power of signals lies.
This is why we say that, typically, Signals don't need useMemo()
. Out-of-the-box, Signals enable you to perform surgical updates of the UI only, which require updating. Signals allow you to skip all of the intermediate parts.
useMemo()
can’t skip component layers. At best, useMemo()
can prune the component tree so there are fewer branches to visit, but updates still require that we descend through these branches.
Signals and memoization may be equivalent, but in reality, signals are a lot more efficient because they are not bound by the component render tree.
They exist on a plane independent from the component tree.
Indeed, Signals do not require memoization, but memoization in React can’t match the surgical nature of signals.
Introducing Visual Copilot: convert Figma designs to high quality code in a single click.