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!