Back to blog

Signals in JavaScript 2026: The Future of Web Reactivity

Hello HaWkers, one of the most anticipated TC39 proposals is advancing in 2026: native Signals in JavaScript. This feature promises to unify how frameworks handle reactivity and could fundamentally change the way we build web applications.

Let's explore what Signals are, how they work, and their impact on the ecosystem.

What Are Signals

Fundamental Concept

// Signal is a reactive container for values

// Simple analogy
const traditionalVariable = 5; // Static value
// If it changes, nobody knows

// Signal
const count = signal(5); // Reactive value
// When it changes, dependents are automatically notified

How They Work

// Conceptual implementation (not the final TC39 API)

// 1. Create a signal
const count = signal(0);

// 2. Read the value
console.log(count.value); // 0

// 3. Write a value
count.value = 1; // Notifies dependents

// 4. Computed (derived from other signals)
const doubled = computed(() => count.value * 2);
// doubled.value is always count.value * 2
// Automatically recalculates when count changes

// 5. Effect (reactive side effect)
effect(() => {
  console.log(`Count is now: ${count.value}`);
});
// Executes whenever count changes

Why Signals Matter

The Current Problem

// React: entire component re-render

function Counter() {
  const [count, setCount] = useState(0);

  // When count changes:
  // 1. Counter function executes again
  // 2. Virtual DOM is created
  // 3. Diff with previous DOM
  // 4. Patches applied

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

// Even though only count changed, everything re-renders
// Signals: granular update

function Counter() {
  const count = signal(0);
  const doubled = computed(() => count.value * 2);

  // When count changes:
  // 1. Only the text using count.value updates
  // 2. Computed doubled recalculates
  // 3. Only the text using doubled updates
  // 4. No component re-render

  return (
    <div>
      <p>Count: {count}</p>
      <p>Doubled: {doubled}</p>
      <button onClick={() => count.value++}>
        Increment
      </button>
    </div>
  );
}

// Surgical update, no overhead

Performance Comparison

// Conceptual benchmark

const performanceComparison = {
  react: {
    update: 'Component re-render',
    complexity: 'O(tree size)',
    overhead: 'Virtual DOM diff',
    ideal: 'UIs with many coordinated changes'
  },

  signals: {
    update: 'Direct DOM mutation',
    complexity: 'O(1) per signal',
    overhead: 'Dependency tracking',
    ideal: 'Frequent and granular updates'
  },

  realWorld: {
    smallApp: 'Negligible difference',
    largeApp: 'Signals 2-10x faster',
    animations: 'Signals much superior',
    forms: 'Signals much superior'
  }
};

Frameworks Already Using Signals

SolidJS

// SolidJS: Signals from the beginning

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

function Counter() {
  // Primitive signal
  const [count, setCount] = createSignal(0);

  // Computed (memo)
  const doubled = createMemo(() => count() * 2);

  // Effect
  createEffect(() => {
    console.log('Count changed:', count());
  });

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

// Note: count() is a function, not .value
// SolidJS uses functions for tracking

Vue 3

// Vue 3: Composition API with Signal-based reactivity

import { ref, computed, watchEffect } from 'vue';

// ref is essentially a Signal
const count = ref(0);

// computed is a derived Signal
const doubled = computed(() => count.value * 2);

// watchEffect is an Effect
watchEffect(() => {
  console.log('Count changed:', count.value);
});

// In Vue template
// <template>
//   <p>Count: {{ count }}</p>
//   <p>Doubled: {{ doubled }}</p>
//   <button @click="count++">Increment</button>
// </template>

Angular

// Angular 17+: Official Signals

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

@Component({
  template: `
    <p>Count: {{ count() }}</p>
    <p>Doubled: {{ doubled() }}</p>
    <button (click)="increment()">Increment</button>
  `
})
export class CounterComponent {
  // Signal
  count = signal(0);

  // Computed
  doubled = computed(() => this.count() * 2);

  constructor() {
    // Effect
    effect(() => {
      console.log('Count changed:', this.count());
    });
  }

  increment() {
    this.count.update(c => c + 1);
    // or: this.count.set(this.count() + 1);
  }
}

Preact Signals

// Preact Signals: works in React too!

import { signal, computed, effect } from '@preact/signals-react';

// Global signals (outside the component)
const count = signal(0);
const doubled = computed(() => count.value * 2);

function Counter() {
  // No hooks! Signal is global and reactive
  return (
    <div>
      <p>Count: {count}</p>
      <p>Doubled: {doubled}</p>
      <button onClick={() => count.value++}>
        Increment
      </button>
    </div>
  );
}

// The component DOESN'T re-render when count changes
// Only the text is updated directly

The TC39 Proposal

Status in 2026

// TC39 Signals Proposal

const tc39Proposal = {
  stage: 'Stage 2 (draft)',
  champions: ['Rob Eisenberg (Angular)', 'Daniel Ehrenberg'],
  goal: 'Standard primitives for reactivity',

  apis: {
    // Basic Signal
    'Signal.State': 'Mutable reactive value',
    'Signal.Computed': 'Derived value (read-only)',
    'Signal.subtle.Watch': 'Low-level API for effects',
  },

  notIncluded: {
    effect: 'Frameworks implement',
    batch: 'Frameworks implement',
    rendering: 'Frameworks implement'
  },

  philosophy: 'Interoperability, not replacement'
};

Proposed API

// TC39 API (subject to changes)

// Signal.State - mutable value
const count = new Signal.State(0);
count.get(); // 0
count.set(1);
count.get(); // 1

// Signal.Computed - derived value
const doubled = new Signal.Computed(() => count.get() * 2);
doubled.get(); // 2
// doubled.set() doesn't exist - it's read-only

// Signal.subtle.Watch - for frameworks
const watcher = new Signal.subtle.Watch(() => {
  // Called when dependencies change
  console.log('Something changed');
});

// Frameworks use this to implement effects

Why Standardize

// The problem of current fragmentation

const currentFragmentation = {
  solidjs: 'createSignal() returns tuple',
  vue: 'ref() uses .value',
  angular: 'signal() uses get/set functions',
  preact: 'signal() uses .value',
  svelte5: 'Runes with $state',
  qwik: 'useSignal() with .value'
};

// With TC39 Signals
const futureInterop = {
  sharedPrimitive: 'Signal.State and Signal.Computed',
  frameworks: 'Thin wrappers over standard primitives',
  libraries: 'Can be framework-agnostic',
  benefit: 'Share reactive code between frameworks'
};

Signals vs useState

When to Use Each

// React with useState (current standard)

function Form() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [errors, setErrors] = useState({});

  // Each setState causes re-render

  return (
    <form>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <input
        value={email}
        onChange={e => setEmail(e.target.value)}
      />
      {/* Re-renders everything on each keystroke */}
    </form>
  );
}

// With Signals (e.g., Preact Signals in React)

const name = signal('');
const email = signal('');
const errors = signal({});

function Form() {
  // Component doesn't re-render!

  return (
    <form>
      <input
        value={name}
        onInput={e => name.value = e.target.value}
      />
      <input
        value={email}
        onInput={e => email.value = e.target.value}
      />
      {/* Granular updates, no re-render */}
    </form>
  );
}

Trade-offs

// When Signals shine
const signalsIdealFor = [
  'Forms with many fields',
  'Animations and transitions',
  'High-frequency updates',
  'Shared state between components',
  'Apps with lots of interactivity'
];

// When useState is sufficient
const useStateIdealFor = [
  'Simple local state',
  'UIs with few changes',
  'When re-render is cheap',
  'When you need effects on re-render'
];

// Considerations
const considerations = {
  devex: 'Signals require different mental model',
  debugging: 'useState easier to debug',
  ecosystem: 'React ecosystem assumes useState',
  future: 'React may add native signals'
};

Implementing Signals Today

In React

// Option 1: Preact Signals
npm install @preact/signals-react

import { signal, computed } from '@preact/signals-react';

// Option 2: Jotai (signal-like)
npm install jotai

import { atom, useAtom } from 'jotai';

const countAtom = atom(0);
const doubledAtom = atom((get) => get(countAtom) * 2);

function Counter() {
  const [count, setCount] = useAtom(countAtom);
  const [doubled] = useAtom(doubledAtom);
  // ...
}

// Option 3: Legend State
npm install @legendapp/state @legendapp/state/react

import { observable } from '@legendapp/state';
import { observer } from '@legendapp/state/react';

const state = observable({ count: 0 });

const Counter = observer(function Counter() {
  return <div>{state.count.get()}</div>;
});

In Vue

// Vue 3 already has signals (refs)

import { ref, computed, watch } from 'vue';

// Reactive composable
function useCounter() {
  const count = ref(0);
  const doubled = computed(() => count.value * 2);

  function increment() {
    count.value++;
  }

  return { count, doubled, increment };
}

// Usage in component
export default {
  setup() {
    const { count, doubled, increment } = useCounter();
    return { count, doubled, increment };
  }
};

In Svelte 5

// Svelte 5 Runes (signals)

<script>
  // $state is a signal
  let count = $state(0);

  // $derived is a computed
  let doubled = $derived(count * 2);

  // $effect is an effect
  $effect(() => {
    console.log('Count is', count);
  });

  function increment() {
    count++; // That simple!
  }
</script>

<button onclick={increment}>
  Count: {count}
</button>
<p>Doubled: {doubled}</p>

The Future With TC39 Signals

2027+ Scenario

// Possible future with native Signals

// Native JavaScript (no imports!)
const count = new Signal.State(0);
const doubled = new Signal.Computed(() => count.get() * 2);

// React could adopt
function Counter() {
  // use() hook for signals
  const countValue = use(count);

  return <p>Count: {countValue}</p>;
}

// Or even native JSX
<p>Count: {count}</p> // Auto-unwrap in JSX

// Framework-agnostic libraries
class ReactiveDatePicker {
  selectedDate = new Signal.State(null);
  formattedDate = new Signal.Computed(() =>
    this.selectedDate.get()?.toLocaleDateString()
  );

  // Works in React, Vue, Angular, Svelte...
}

Ecosystem Impact

// What changes with native Signals

const ecosystemImpact = {
  frameworks: {
    change: 'Thin wrappers over native primitives',
    benefit: 'Less code, more performance',
    risk: 'Less differentiation between frameworks'
  },

  libraries: {
    change: 'Can be framework-agnostic',
    benefit: 'Write once, use anywhere',
    example: 'Form library that works in React and Vue'
  },

  learning: {
    change: 'Signal concept becomes fundamental',
    benefit: 'Knowledge transfer between frameworks',
    requirement: 'Understanding reactivity becomes essential'
  },

  performance: {
    change: 'Optimizations in JavaScript engine',
    benefit: 'Signals faster than any userland implementation',
    expectation: 'V8, SpiderMonkey optimize for signals'
  }
};

Conclusion

Signals represent a fundamental shift in how we think about reactivity on the web. While React's re-render model dominated the last decade, signals offer a more granular and performant alternative.

What to do now:

  1. Understand the concept: Even without using them, understand how signals work
  2. Experiment: Try Preact Signals, Vue 3, or Angular 17+
  3. Follow TC39: The proposal is advancing
  4. Don't abandon React: Signals can complement, not replace

The future seems to converge on signals as the standard reactivity primitive. Frameworks will continue to exist, but with a common foundation that facilitates interoperability and learning.

To understand more about the current JavaScript ecosystem, read: VoidZero in 2026.

Let's go! 🦅

Comments (0)

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

Add comments