Adam L Barrett PRO
Adam L Barrett is a JavaScript and Front-End consultant, a contributor to open source, and avid bike-shedder.
adamlbarrett.bsky.social
BigAB
Use this QR Code to let me know what you thought of this talk!
🙄 ...It's just a monoid in the category of endofunctors, what's the problem?
- Anon Functional Programmer Bro
# JS Signals
🧑🏻💻
🏃🏽♀️
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
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 |
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
(Better than stores)
🧑🏻💻
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
🏃🏽♀️
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
Value
* 2
2
* -2
$ + $
1
-2
0
log
0
Value
* 2
2
* -2
$ + $
2
-2
0
log
0
Value
* 2
4
* -2
$ + $
2
-2
0
log
0
Value
* 2
4
* -2
$ + $
2
-2
2
log
2
Value
* 2
4
* -2
$ + $
2
-2
2
log
2
Value
* 2
4
* -2
$ + $
2
-4
2
log
2
Value
* 2
4
* -2
$ + $
2
-4
0
log
0
Value
* 2
4
* -2
$ + $
2
-4
0
log
0
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
adamlbarrett.bsky.social
BigAB
Use this QR Code to let me know what you thought of this talk!
By Adam L Barrett
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.
Adam L Barrett is a JavaScript and Front-End consultant, a contributor to open source, and avid bike-shedder.