Telerik blogs

Take a look at how some of the most popular JavaScript libraries handle state, with or without signals, and decide if it’s a feature you’ll look for in the future.

It seems that most frameworks now use signals to handle reactive state. First we saw them in SolidJS and Vue, then Preact and Qwik, and now Angular. We don’t always need signals, but they can make things easy. Let’s see how state is handled in each framework.

No Signals

React

import { useState } from "react";

export default function App() {
  const [x, setX] = useState(1);
  const x2 = x * 2;

  return (
    <>
      <p>{x2}</p>
      <button onClick={() => setX(x + 1)}>Double</button>
    </>
  );
}

See the CodeSandbox.

React has been using useState since 2018. It dramatically decreased complexity from the Class components. While it works great, every child component will get re-rendered unless you memoize the component with useMemo or useCallback, which could result in more complex rendering. Some people solve this by directly importing @preact/signals-react into their react project. useState can also support deep reactive values if you reset the whole object.

RXJS

import { ChangeDetectionStrategy, Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BehaviorSubject, map } from 'rxjs';

@Component({
  selector: 'app-rxjs',
  standalone: true,
  imports: [CommonModule],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <p>{{ x1 | async }}</p>
    <button (click)="increment()">Double</button>
  `
})
export class RxjsComponent { 
  readonly x = new BehaviorSubject(1);
  readonly x1 = this.x.pipe(map((v) => v * 2));

  increment() {
    this.x.next(this.x.value + 1);
  }
}

See the Angular StackBlitz.

Angular before signals relies heavily on RXJS. While RXJS pretty much solves the problem (even without ZoneJS), it can be a beast and hard to understand if you have not come close to mastering it.

You have to subscribe to observables or subjects, and, even with declarative programming pipes, combineLatest can have side effects for updated values including the reactive diamond problem.

Overall, these are nonstarters issues if you’ve mastered RXJS, as ultimately RXJS is built for complexity, not simplicity. RXJS can support any type of object if you use the right operators. Powerful, just not simple.

Svelte

<script>
	import { writable, derived } from 'svelte/store';
	
	const x = writable(1);
	const x1 = derived(x, $x => $x * 2);
</script>

<p>{$x1}</p>
<button on:click={() => x.update(v => ++v)}>Double</button>

See the Svelte REPL.

Svelte does not need signals, as the idea for its writable stores is pretty much copied from RXJS and simplified. In fact, if you need to handle async data with race conditions, Svelte stores are interoperable with RXJS. Like Signals, Svelte stores just target the necessary part of the UI that needs updating. You have to replace the whole object for deep reactive updates, but this can be easily done with update. Compiler magic is amazing.

Signals

Vue

<script setup>
import { ref, computed } from 'vue'

const x = ref(1)
const x1 = computed(() => x.value * 2)

</script>

<template>
  <p>{{ x1 }}</p>
  <button @click="x++">Double</button>
</template>

See the Vue Playground.

In Vue, Signals are just ref. Since the reactive store doesn’t handle computed values out of the box, Vue has ref. Vue handles deep reactivity by default with objects, and also has the reactive function. Signals just make Vue better.

Qwik

import { component$, useSignal, useComputed$ } from '@builder.io/qwik';

export default component$(() => {
  
  const x = useSignal(1);
  const x1 = useComputed$(() => 2 * x.value);
  
  return <>
    <p>{x1}</p>
    <button onClick$={() => x.value++}>Double</button>
  </>;
});

See the Qwik Playground.

Qwik just saw what worked well with Svelte and Vue and decided to have signals that work as you would expect. You can get reactive nested objects with useStore instead of useSignal. Of course, Qwik automatically serializes the values from the server to the browser, which no other framework does. With Signals and Resumability, Qwik pretty much wins the UX contest.

SolidJS

import { render } from "solid-js/web";
import { createSignal } from "solid-js";

function Counter() {

  const [x, setX] = createSignal(1);
  const x1 = () => x() * 2;

  return <>
    <p>{x1()}</p>
    <button type="button" onClick={() => setX(x() + 1)}>Double</button>
  </>;
}

render(() => <Counter />, document.getElementById("app")!);

See the SolidJS Playground.

Solid uses an interesting approach by calling the signals as a function. This ensures the values are reactive at the proper times. They are also destructured like React’s useState, since SolidJS is meant to be the fastest JS Framework out there, heavily inspired by React. Preact could never be this fast, though it retains React library compatibility. SolidJS also supports nested reactive values with createStore. Solid is perhaps the fastest pure JavaScript framework out there, with signals being a reason.

Preact

import { render } from 'preact';
import { signal, computed } from "@preact/signals@1.1.0";

const x = signal(1);
const x1 = computed(() => x.value * 2);

function Counter() {
  return <>
	  <p>{x1}</p>
    <button onClick={() => x.value++}>Double</button>
  </>;
}

render(<Counter />, document.getElementById('app'));

See the Preact REPL.

Preact solved the problem before React has by creating their own version of signals. Heavily inspired by Vue and SolidJS, its signals are extremely simple to use. You get nested reactivity by passing back in the whole changed object. I think I heard more people use the Preact signals library in React than there are Preact users?

Angular

import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
  <p>{{ x2() }}</p>
  <button (click)="increment()">Double</button>
  `
})
export class AppComponent {

  readonly x = signal(1);
  readonly x2 = computed(() => this.x() * 2);

  increment() {
    this.x.update((x) => ++x);
  }
}

See the Angular StackBlitz.

Finally we have Angular Signals. They seem to be simple like Preact signals, but also use the function approach like SolidJS. Additionally, you can convert back and forth to an observable with fromObservable and fromSignal.

Instead of checking the entire component tree for changes, they will update only what is changed. While they don’t handle async data (we have RXJS for that), they do increase performance and decrease complexity.

Soon we shall see the ability to remove ZoneJS for good from Angular. You cannot talk about Angular reactivity without checking out Joshua Morony’s Channel.

While Svelte doesn’t need them, signals are the future for every single framework except the biggest one. Will React ever get signals? No clue. The keyword for Signals is “granularity.” You update only what has changed. I personally will be using a framework with Signals going forward.


About the Author

Jonathan Gamble

Jonathan Gamble has been an avid web programmer for more than 20 years. He has been building web applications as a hobby since he was 16 years old, and he received a post-bachelor’s in Computer Science from Oregon State. His real passions are language learning and playing rock piano, but he never gets away from coding. Read more from him at https://code.build/.

 

 

Related Posts

Comments

Comments are disabled in preview mode.