JS Signals

adamlbarrett.bsky.social

BigAB

Use this QR Code to let me know what you thought of this talk!

JS Signals?

What are JS Signals?

A not-so-new,

reactive state primitive

poorly named,

 🙄 ...It's just a monoid in the category of endofunctors, what's the problem?

- Anon Functional Programmer Bro

What makes then so special?

# JS Signals

 

🧑🏻‍💻

Developer Experience

🏃🏽‍♀️

Performance

What are JS Signals?

A not-so-new, poorly named, reactive state primitive

reactive state primitive

A not-so-new, poorly named, reactive state primitive

What do we mean by "reactive"?

We are not talking about:

  • Systems that are Responsive, Resilient, Elastic and Message Driven (reactivemanifesto.org)
  • Programming with asynchronous data streams
  • An event-based model where data is pushed to the consumer, as it becomes available

Reactive

  • We have a value
  • When that value changes...
  • Anything "using" that value also changes   ...automatically

An Exploration of Reactvity in JavaScript

const button = document.querySelector('button');
const output = document.querySelector('output');

let count = 1;
let double = count * 2;

button.addEventListener('click', () => {
  count++;
});

const updateUI = () => {
  output.innerHTML = `
    <h1>${count} * 2 = ${double}</h1>
  `;
};
updateUI();
# Reactivity
const button = document.querySelector('button');
const output = document.querySelector('output');

let count = 1;
let double = count * 2;

button.addEventListener('click', () => {
  count++;
});

const updateUI = () => {
  output.innerHTML = `
    <h1>${count} * 2 = ${double}</h1>
  `;
};
updateUI();
# Reactivity
const button = document.querySelector('button');
const output = document.querySelector('output');

let count = 1;
let double = count * 2;

button.addEventListener('click', () => {
  count++;
  double = count * 2;
  updateUI();
});

const updateUI = () => {
  output.innerHTML = `
    <h1>${count} * 2 = ${double}</h1>
  `;
};
updateUI();
# Reactivity
export const Observable = (fn) => ({ subscribe: fn });

export const observe = (element, event) => {
  return Observable((observer) => {
    element.addEventListener(event, observer);
    return () => {
      element.removeEventListener(event, observer);
    };
  });
};
# Reactivity
export const map = (observable, fn) => {
  return Observable((observer) => {
    return observable.subscribe((value) => {
      observer(fn(value));
    });
  });
};

export const scan = (observable, fn, initialState) => {
  let state = initialState;
  let unsub;
  const observers = new Set();
  return Observable((observer) => {
    observers.add(observer);
    observer(state);
    if (observers.size === 1) {
      unsub = observable.subscribe((value) => {
        state = fn(state, value);
        observers.forEach((obs) => obs(state));
      });
    }
    return () => {
      observers.delete(observer);
      if (observers.size === 0) {
        unsub();
      }
    };
  });
};

export const combine = (...observables) => {
  return Observable((observer) => {
    const values = new Array(observables.length);

    const subscriptions = observables.map(
      (observable, index) =>
        observable.subscribe((value) => {
          values[index] = value;
          if (
            values.every((value) => value !== undefined)
          ) {
            observer(values);
          }
        })
    );

    return () => {
      subscriptions.forEach((unsubscribe) => unsubscribe());
    };
  });
};

export const pipe =
  (...fns) =>
  (value) =>
    fns.reduce((acc, fn) => fn(acc), value);
# Reactivity
import { observe, scan, map, combine } from './observable';

const button = document.querySelector('button');
const output = document.querySelector('output');

const clicks = observe(button, 'click');

const count = scan(clicks, (v) => v + 1, 1);
const double = map(count, (n) => n * 2);

combine(count, double).subscribe(([n, dbl]) => {
  output.innerHTML = `
    <h1>${n} * 2 = ${dbl}</h1>
  `;
});
# Reactivity
import { fromEvent, startWith, scan, map, zip } from 'rxjs';

const button = document.querySelector('button');
const output = document.querySelector('output');

const clicks = fromEvent(button, 'click');

const count = clicks.pipe(
  startWith(1),
  scan((count) => count + 1)
);
const double = count.pipe(map((count) => count * 2));

zip(count, double).subscribe(([count, double]) => {
  output.innerHTML = `
    <h1>${count} * 2 = ${double}</h1>
  `;
});
# Reactivity
export const createStore = (state) => {
  let listeners = new Set();

  return {
    get: () => state,
    set: (updater = (v) => v) => {
      state = updater(state);
      listeners.forEach((listener) => listener(state));
    },
    subscribe: (listener) => {
      listeners.add(listener);
      listener(state);
      return () => {
        listeners.delete(listener);
      };
    },
  };
};
# Reactivity
import { createStore, derived } from './store.js';

const button = document.querySelector('button');
const output = document.querySelector('output');

const count = createStore(1);
const double = derived([count], ([count]) => count * 2);

button.addEventListener('click', () =>
  count.set((count) => count + 1)
);

derived([count, double]).subscribe(([count, double]) => {
  output.innerHTML = `
    <h1>${count} * 2 = ${double}</h1>
  `;
});
# Reactivity
import { createStore, derived } from './store.js';

const button = document.querySelector('button');
const output = document.querySelector('output');

const count = createStore(1);
const double = derived([count], ([count]) => count * 2);

button.addEventListener('click', () =>
  count.set((count) => count + 1)
);

derived([count, double]).subscribe(([count, double]) => {
  output.innerHTML = `
    <h1>${count} * 2 = ${double}</h1>
  `;
});
# Reactivity
import { useSyncExternalStore } from 'react';
import { createStore, derived } from './store.js';

const count = createStore(1);
const double = derived([count], ([count]) => count * 2);

const syncExternalStore = derived([count, double]);
const handleClick = () => count.set((v) => v + 1);

export default function App() {
  const [cnt, dbl] = useSyncExternalStore(
    syncExternalStore.subscribe,
    syncExternalStore.get
  );
  return (
    <>
      <output>
        <h1>
          {cnt} * 2 = {dbl}
        </h1>
      </output>
      <button onClick={handleClick}>+1</button>
    </>
  );
}
# Reactivity
<script>
  import { writable, derived } from 'svelte/store';

  const count = writable(1);
  const double = derived(count, (n) => n * 2);

  const handleClick = () => $count += 1;
</script>

<output>
	<h1>
		{$count} * 2 = {$double}
	</h1>
</output>
<button on:click={handleClick}>+1</button>
# Reactivity
import { signal, computed, effect } from './signals';

const button = document.querySelector('button');
const output = document.querySelector('output');

let count = signal(1);
let double = computed(() => count.value * 2);

button.addEventListener('click', () => count.value++);

effect(() => {
  output.innerHTML = `
    <h1>${count.value} * 2 = ${double.value}</h1>
  `;
});
# Reactivity
import { useSignal, useComputed } from '@preact/signals-react';

export default function App() {
  const count = useSignal(0);
  const double = useComputed(() => count.value * 2);

  const handleClick = () => count.value++;
  return (
    <>
      <output>
        <h1>
          {count} * 2 = {double}
        </h1>
      </output>
      <button onClick={handleClick}>+1</button>
    </>
  );
}
# Reactivity

So what do we mean by "reactive"?

reactive state primitive

A not-so-new, poorly named, reactive state primitive

State

Primitive

A not-so-new, poorly named, reactive state primitive

A not-so-new, 

Framework since Reactive State Derived Values Side Effect
Knockout 2010 observable pureComputed computed
CanJS 2012 observable compute compute.on()
Svelte 2016 let variable = ? $: (Reactive Values) $: (reactive statements)
Solid 2018 createSignal createMemo createEffect
Vue 2020 ref/reactive computed watch/watchEffect
Preact 2022 signal computed effect
Qwik 2022 useSignal/useStore useComputed$ / useResource$ useTask$
Angular 2023 signal computed effect
Svelte 5 2024 $state $derived $effect

What are JS Signals?

import { signal, computed, effect } from './signals';

const button = document.querySelector('button');
const output = document.querySelector('output');

let count = signal(1);
let double = computed(() => count.value * 2);

button.addEventListener('click', () => count.value++);

effect(() => {
  output.innerHTML = `
    <h1>${count.value} * 2 = ${double.value}</h1>
  `;
});
# What are JS Signals

JS Signals

State

Derived

Effect

JS Signals

Auto Tracking

Acyclic

Push-then-Pull

Incremental Improvement

EventTarget

Observable

Stores

Signals

So how are they better?

(Better than stores)

JS Signals

State

Derived

Effect

JS Signals

Auto Tracking

Acyclic

Push-then-Pull

🧑🏻‍💻

Developer Experience

Auto Tracking

import { signal, computed, effect } from './signals';

const button = document.querySelector('button');
const output = document.querySelector('output');

let count = signal(1);
let double = computed(() => count.value * 2);

button.addEventListener('click', () => {
  count.value++;
});

effect(() => {
  output.innerHTML = `
    <h1>${count.value} * 2 = ${double.value}</h1>
  `;
});
# What are JS Signals
import { signal, computed, effect } from './signals';

const button = document.querySelector('button');
const output = document.querySelector('output');

let count = signal(1);
let double = computed(() => count() * 2);

button.addEventListener('click', () => {
  count.update((n) => n + 1)
});

effect(() => {
  output.innerHTML = `
    <h1>${count()} * 2 = ${double()}</h1>
  `;
});
# What are JS Signals
import { signal, computed, effect } from './signals';

const button = document.querySelector('button');
const output = document.querySelector('output');

let count = signal(1);
let double = computed(() => count.get() * 2);

button.addEventListener('click', () => {
  count.set(count.get() + 1)
});

effect(() => {
  output.innerHTML = `
    <h1>${count.get()} * 2 = ${double.get()}</h1>
  `;
});
# What are JS Signals
<script>
  let count = $state(1);
  let double = $derived(count * 2);

  const handleClick = () => count++;
</script>

<output>
	<h1>
		{count} * 2 = {double}
	</h1>
</output>
<button onclick={handleClick}>+1</button>
# Reactivity

🏃🏽‍♀️

Performance

Push-then-Pull

import { signal, computed, effect } from './signals';

const button = document.querySelector('button');
const output = document.querySelector('output');

let count = signal(1);
let double = computed(() => count.value * 2);

button.addEventListener('click', () => {
  count.value++;
});

effect(() => {
  output.innerHTML = `
    <h1>${count.value} * 2 = ${double.value}</h1>
  `;
});
# What are JS Signals

Count

1

Count

1

import { signal, computed, effect } from './signals';

const button = document.querySelector('button');
const output = document.querySelector('output');

let count = signal(1);
let double = computed(() => count.value * 2);

button.addEventListener('click', () => {
  count.value++;
});

effect(() => {
  output.innerHTML = `
    <h1>${count.value} * 2 = ${double.value}</h1>
  `;
});
# What are JS Signals

Count

1

Double

import { signal, computed, effect } from './signals';

const button = document.querySelector('button');
const output = document.querySelector('output');

let count = signal(1);
let double = computed(() => count.value * 2);

button.addEventListener('click', () => {
  count.value++;
});

effect(() => {
  output.innerHTML = `
    <h1>${count.value} * 2 = ${double.value}</h1>
  `;
});
# What are JS Signals

Count

1

Double

Effect

Count

1

Double

Effect

Count

1

Double

Effect

Count

1

Double

Effect

Count

1

Double

Effect

Count

1

Double

Effect

Count

1

Double

Effect

Count

1

Double

Effect

Count

1

Double

Effect

Count

1

Double

Effect

Count

1

Double

Effect

2

Count

1

Double

Effect

2

Count

1

Double

Effect

2

import { signal, computed, effect } from './signals';

const button = document.querySelector('button');
const output = document.querySelector('output');

let count = signal(1);
let double = computed(() => count.value * 2);

button.addEventListener('click', () => {
  count.value++;
});

effect(() => {
  output.innerHTML = `
    <h1>${count.value} * 2 = ${double.value}</h1>
  `;
});
# What are JS Signals

Count

2

Double

Effect

2

Count

2

Double

Effect

2

Count

2

Double

Effect

2

Count

2

Double

Effect

2

Count

2

Double

Effect

2

Count

2

Double

Effect

2

import { signal, computed, effect } from './signals';

const button = document.querySelector('button');
const output = document.querySelector('output');

let count = signal(1);
let double = computed(() => count.value * 2);

button.addEventListener('click', () => {
  count.value++;
});

effect(() => {
  output.innerHTML = `
    <h1>${count.value} * 2 = ${double.value}</h1>
  `;
});
# What are JS Signals

Count

2

Double

Effect

2

Count

2

Double

Effect

2

Count

2

Double

Effect

2

Count

2

Double

Effect

2

Count

2

Double

Effect

2

Count

2

Double

Effect

2

Count

2

Double

Effect

2

Count

2

Double

Effect

2

Count

2

Double

Effect

2

Count

2

Double

Effect

2

Count

2

Double

Effect

2

Count

2

Double

Effect

4

Count

2

Double

Effect

4

# What are JS Signals

The Key Takeaways

  • the "reads" set up the singal trees sink/source relationships
  • there are 2 stages, the notify stage and the read
  • everything is read lazily and only if it might have been changed in the notify stage

The Diamond Problem

The Diamond Problem

Value

* 2

2

* -2

$ + $

1

-2

0

log

0

The Diamond Problem

Value

* 2

2

* -2

$ + $

2

-2

0

log

0

The Diamond Problem

Value

* 2

4

* -2

$ + $

2

-2

0

log

0

The Diamond Problem

Value

* 2

4

* -2

$ + $

2

-2

2

log

2

The Diamond Problem

Value

* 2

4

* -2

$ + $

2

-2

2

log

2

The Diamond Problem

Value

* 2

4

* -2

$ + $

2

-4

2

log

2

The Diamond Problem

Value

* 2

4

* -2

$ + $

2

-4

0

log

0

The Diamond Problem

Value

* 2

4

* -2

$ + $

2

-4

0

log

0

A not-so-new, poorly named, reactive state primitive

poorly named,

Developer Hapiness

let currentEffect = null;

export const signal = (val) => {
  return {
    sinks: new Set(),
    get value() {
      if (currentEffect) {
        this.sinks.add(currentEffect);
        currentEffect.sources.add(this);
      }
      return val;
    },
    set value(newValue) {
      val = newValue;
      [...this.sinks].forEach((sink) => sink.run());
    },
  };
};

export const effect = (fn) => {
  const _effect = {
    sources: new Set(),
    run() {
      this.sources.forEach((source) => {
        source.sinks.delete(this);
      });
      this.sources.clear();

      let prev = currentEffect;
      currentEffect = this;
      fn();
      currentEffect = prev;
    },
  };
  _effect.run();
};

export const computed = (fn) => {
  const s = signal();
  effect(() => (s.value = fn()));
  return s;
};
# Live Coding Result

?

Questions?

adamlbarrett.bsky.social

BigAB

Use this QR Code to let me know what you thought of this talk!

JS Signals

JS Signals ConFoo 2025

By Adam L Barrett

JS Signals ConFoo 2025

JavaScript has a new primitive for reactive programming: Signals. Almost all the major frameworks have adopted them and there is even a TC39 proposal to add signals directly into the language. But what do they do? In this talk we will dig deep into the (imho) poorly named Signal, discuss how they are used in modern programming, demystify them by implementing our own in JS, and address the current status of the ECMAScript Signals proposal.

  • 74