Back to blog

Signals in JavaScript: The Future of Reactivity That Could Change the Web

Hello HaWkers, if you work with modern JavaScript frameworks, you've probably heard about Signals. What started as an alternative approach to state management is becoming a consensus among Angular, Vue, Solid, Svelte, and potentially entering JavaScript's official specification.

Let's understand what Signals are, why they're conquering the community, and how they could change the way we write web applications.

What Are Signals

Fundamental Concept

Signals are reactive primitives that store a value and automatically notify their dependents when that value changes. Unlike the traditional model of complete re-rendering, Signals allow surgical updates only where needed.

Basic anatomy of a Signal:

// Creating a signal
const count = signal(0)

// Reading the value
console.log(count()) // 0

// Updating the value
count.set(1)
// or
count.update(n => n + 1)

// Deriving values (computed)
const doubled = computed(() => count() * 2)

// Reacting to changes (effect)
effect(() => {
  console.log(`Count is: ${count()}`)
})

Why Signals Are Different

The magic of Signals lies in fine-grained reactivity. Compare with traditional approaches:

React (complete re-render):

function Counter() {
  const [count, setCount] = useState(0)
  const [name, setName] = useState('User')

  // When count changes, the ENTIRE component re-renders
  // Including parts that only depend on name
  return (
    <div>
      <h1>Hello, {name}</h1>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>+</button>
    </div>
  )
}

With Signals (surgical update):

function Counter() {
  const count = signal(0)
  const name = signal('User')

  // When count changes, ONLY the count text updates
  // The h1 with name is not touched
  return (
    <div>
      <h1>Hello, {name()}</h1>
      <p>Count: {count()}</p>
      <button onClick={() => count.update(c => c + 1)}>+</button>
    </div>
  )
}

The Framework Convergence

Who Is Using Signals

In 2026, practically all JavaScript frameworks have adopted or are adopting Signals.

Status by framework:

Framework Signals Status Since
Solid.js Native (pioneer) 2021
Angular Signals API 2023
Vue 3.6 Alien Signals 2026
Svelte 5 Runes ($state) 2024
Preact @preact/signals 2022
Qwik Native Signals 2022
React Under discussion -

The TC39 Proposal

Most interesting is that Signals may become part of native JavaScript. There's an active proposal at TC39 to add Signals to the specification.

Proposal status:

TC39 Proposal: Signals
Stage: 1 (Proposal)
Champions: Daniel Ehrenberg, Rob Riggs

Goal: Create a standardized reactivity primitive
that works as a foundation for all frameworks

Proposed native API:

// Possible native API (speculative)
const count = Signal.state(0)
const doubled = Signal.computed(() => count.get() * 2)

Signal.effect(() => {
  console.log(doubled.get())
})

count.set(5) // Logs: 10

How Signals Work Internally

The Dependency System

Signals use a dependency graph that is automatically built during execution.

// Internally, something like this happens:

class Signal {
  #value
  #subscribers = new Set()

  constructor(initialValue) {
    this.#value = initialValue
  }

  get() {
    // If we're inside an effect/computed,
    // register this dependency
    if (currentTracking) {
      this.#subscribers.add(currentTracking)
    }
    return this.#value
  }

  set(newValue) {
    if (this.#value !== newValue) {
      this.#value = newValue
      // Notify all subscribers
      this.#subscribers.forEach(sub => sub.notify())
    }
  }
}

Automatic Tracking

Dependency tracking happens automatically when you read a signal inside a reactive context.

const firstName = signal('John')
const lastName = signal('Doe')
const age = signal(30)

// This computed depends ONLY on firstName and lastName
// age is not a dependency because it was not read
const fullName = computed(() => {
  return `${firstName()} ${lastName()}`
})

// Changing age does NOT recalculate fullName
age.set(31) // fullName doesn't react

// Changing firstName recalculates fullName
firstName.set('Jane') // fullName recalculates

Push vs Pull

Signals use a hybrid push-pull model:

// PUSH: When a signal changes, it "pushes" notification
// to its direct dependents

const a = signal(1)
const b = computed(() => a() * 2) // b depends on a
const c = computed(() => b() + 10) // c depends on b

a.set(2) // Push: a notifies b, b notifies c

// PULL: Values are only recalculated when read
// (lazy evaluation)

console.log(c()) // Pull: c calculates b, b calculates a

Practical Implementations

Signals in Angular

Angular introduced Signals as an alternative to RxJS for simple cases.

import { signal, computed, effect } from '@angular/core'

@Component({
  selector: 'app-counter',
  template: `
    <h1>Count: {{ count() }}</h1>
    <h2>Doubled: {{ doubled() }}</h2>
    <button (click)="increment()">+</button>
  `
})
export class CounterComponent {
  count = signal(0)
  doubled = computed(() => this.count() * 2)

  constructor() {
    effect(() => {
      console.log(`Count changed to: ${this.count()}`)
    })
  }

  increment() {
    this.count.update(n => n + 1)
  }
}

Signals in Solid.js

Solid was the pioneer in modern Signals.

import { createSignal, createEffect, createMemo } from 'solid-js'

function Counter() {
  const [count, setCount] = createSignal(0)
  const doubled = createMemo(() => count() * 2)

  createEffect(() => {
    console.log(`Count is ${count()}`)
  })

  return (
    <div>
      <p>Count: {count()}</p>
      <p>Doubled: {doubled()}</p>
      <button onClick={() => setCount(c => c + 1)}>+</button>
    </div>
  )
}

Signals in Vue 3.6

Vue is introducing Signals through Vapor Mode.

<script setup>
import { signal, computed, effect } from 'vue/vapor'

const count = signal(0)
const doubled = computed(() => count() * 2)

effect(() => {
  console.log(`Count: ${count()}`)
})

function increment() {
  count.set(count() + 1)
}
</script>

<template>
  <div>
    <p>Count: {{ count() }}</p>
    <p>Doubled: {{ doubled() }}</p>
    <button @click="increment">+</button>
  </div>
</template>

Advanced Patterns with Signals

Complex Derived Signals

// Multiple dependencies
const items = signal([
  { name: 'Apple', price: 1.5, qty: 3 },
  { name: 'Banana', price: 0.75, qty: 5 },
])

const taxRate = signal(0.1)

// Computed that depends on items and taxRate
const total = computed(() => {
  const subtotal = items().reduce(
    (sum, item) => sum + item.price * item.qty,
    0
  )
  return subtotal * (1 + taxRate())
})

// Updating any dependency recalculates total
items.update(i => [...i, { name: 'Orange', price: 2, qty: 2 }])

Effects with Cleanup

// Effects can return cleanup function
const userId = signal(1)

effect(() => {
  const id = userId()

  // Setup: connect WebSocket
  const ws = new WebSocket(`/user/${id}/updates`)

  ws.onmessage = (event) => {
    console.log('Update:', event.data)
  }

  // Cleanup: disconnect when userId changes
  return () => {
    ws.close()
  }
})

userId.set(2) // Closes old WS, opens new

Update Batching

// Multiple updates in sequence
const a = signal(1)
const b = signal(2)
const c = signal(3)

const sum = computed(() => a() + b() + c())

effect(() => {
  console.log('Sum:', sum())
})

// Without batching: 3 recalculations
a.set(10)
b.set(20)
c.set(30)

// With batching: 1 recalculation
batch(() => {
  a.set(10)
  b.set(20)
  c.set(30)
})

Signals vs Other Approaches

Comparison with Redux

// Redux: Lots of boilerplate
const store = createStore(reducer)
const mapStateToProps = state => ({ count: state.count })
const mapDispatchToProps = { increment }
connect(mapStateToProps, mapDispatchToProps)(Component)

// Signals: Straight to the point
const count = signal(0)
const increment = () => count.update(n => n + 1)

Comparison with RxJS

// RxJS: Powerful but complex
const count$ = new BehaviorSubject(0)
const doubled$ = count$.pipe(map(n => n * 2))
doubled$.subscribe(console.log)
count$.next(5)

// Signals: Simpler for common cases
const count = signal(0)
const doubled = computed(() => count() * 2)
effect(() => console.log(doubled()))
count.set(5)

When to Use Each Approach

Scenario Best Option
Component local state Signals
Complex data streams RxJS
Simple global state Signals
Complex global state Redux/Zustand
Async data Signals + async
Event sourcing RxJS

Signals Performance

Benchmarks

Signals offer superior performance in frequent update scenarios.

Test: 10,000 cascading updates:

Approach Time Memory
Signals (Solid) 12ms 2.1MB
Signals (Preact) 15ms 2.4MB
Vue 3 (ref) 45ms 4.2MB
React (useState) 180ms 8.5MB
MobX 28ms 3.8MB

Why Signals are fast:

  1. No VDOM diff - Direct DOM updates
  2. Precise tracking - Only recalculates what's needed
  3. Lazy evaluation - Computes only when read
  4. Automatic batching - Groups updates

Common Optimizations

// 1. Avoid unnecessary computeds
// Bad
const doubled = computed(() => count() * 2)
const quadrupled = computed(() => doubled() * 2) // Chaining

// Better
const quadrupled = computed(() => count() * 4)

// 2. Use untrack for non-reactive reads
import { untrack } from 'solid-js'

effect(() => {
  const currentCount = count()
  // This read does NOT create dependency
  const config = untrack(() => configSignal())
  console.log(currentCount, config)
})

// 3. Memoize heavy computations
const expensiveResult = computed(() => {
  return items().map(item => heavyComputation(item))
}, { equals: deepEquals })

The Future of Signals

TC39 Standardization

If the TC39 proposal is approved, we'll have native Signals in JavaScript.

Estimated timeline:

  • 2024: Stage 1 (current)
  • 2025: Stage 2 (Draft)
  • 2026-2027: Stage 3 (Candidate)
  • 2028+: Stage 4 (Finished)

Benefits of standardization:

  1. Interoperability between frameworks
  2. Performance optimized by engines
  3. Fewer external dependencies
  4. Consistent API

Impact on Frameworks

With native Signals, frameworks could:

// Today: Each framework has its implementation
import { signal } from 'solid-js' // Solid
import { ref } from 'vue' // Vue
import { signal } from '@angular/core' // Angular

// Future: All use the same base
import { Signal } from 'std:signals' // Native

React and Signals

React hasn't officially adopted Signals yet, but there are discussions.

Perspectives:

  • React Forget (compiler) solves part of the problem
  • Signals would contradict React's mental model
  • Possible partial adoption or official library
  • Community already uses @preact/signals-react

Conclusion

Signals represent a natural evolution in JavaScript state management. The convergence of major frameworks around this primitive, combined with the TC39 proposal, suggests that Signals will be a fundamental part of web development in the coming years.

Key points:

  1. Signals offer fine-grained and performant reactivity
  2. Angular, Vue, Solid, Svelte have already adopted
  3. TC39 proposal may make Signals native
  4. Superior performance to traditional approaches
  5. Simple and intuitive API

Recommendations:

  • Learn the Signals API of your framework
  • Try Solid.js to understand pure Signals
  • Follow the TC39 proposal
  • Consider Signals for new projects

The future of web reactivity is being defined now, and Signals are at the center of this transformation.

For more on framework evolution, read: Vue 3.6 Vapor Mode: The Performance Revolution.

Let's go! 🦅

Comments (0)

This article has no comments yet 😢. Be the first! 🚀🦅

Add comments