Harnessing the Power of Signals
There are so many definitions for reactivity, but I think the simplest one is
Ryan Carniato
A reactive system will make sure this expression is always up to date
Change in data would trigger a large amount of JavaScript to execute.
This meant that fast-changing properties such as animation would cause performance issues.
The framework would eventually reconcile all of the changes into the UI.
< 17
without vapor
State changes only update the part of the UI to which the state is bound to.
The hard part is knowing how to listen to property changes in a way that has good DX.
>=17
with vapor
Scheduler (Concurrent updates, tasks, etc)
Dependency tracking (Automatic, Manual)
Algorithm (Push, Pull, Hybrid, Lazy, Eager, etc)
Other implementation details
vs
In JS we have two mainstream reactive primitives
Reactive Streams (RxJS) - a series/collection of data events that are emitted over time.
const a = 1;
const b = 2;
const c = a + b;import { map, combineLatest, BehaviorSubject } from 'rxjs';
// reactive wrappers, that always respond with latest value
const a$ = new BehaviorSubject(1);
const b$ = new BehaviorSubject(2);
// derived/computed state using operators
const c$ = combineLatest(a$, b$).pipe(map(([a, b]) => a + b));
// lazy
// effects, subscriptions are explicit
c$.subscribe((c) => console.log(c)); // 3
// push based
a$.next(2);
Observables/Subjects
Signals
const a = 1;
const b = 2;
const c = a + b;Signals are fundamentally a simple pub/sub system that holds a value and represents that value over time.
import { signal, computed, effect } from 'some-signals-library';
const a = signal(1)
const b = signal(2)
// dervied without memo
const c = () => a.value + b.value;
// or with memo
// depending on the reactivity alogorthim
// this could run immediately (eager) or on read
const cachedC = computed(() => a.value + b.value);
effect(() => console.log(c.value));
// update the wrapped value;
a.value += 1;Signals
const a = 1
const b = 2
const c = a + b// .svelte
<script>
// it looks like js vars, but it is not
let a = 1
let b = 2
// computed/derived state, compiler will transform this to
// code that keeps this relation alive
$: c = a + b
b += 3
</script>
<p>{c}</p>
< v5
under the hood
export function createSignal(value) {
const read = () => value;
const write = (nextValue) => value = nextValue;
return [read, write];
}Read/Write
const [count, setCount] = createSignal(3);
console.log("Initial Read", count()); // 3
setCount(5);
console.log("Updated Read", count()); // 5
setCount(count() * 2);
console.log("Updated Read", count()); // 10const context = [];
function subscribe(running, subscriptions) {
subscriptions.add(running);
running.dependencies.add(subscriptions);
}
export function createSignal(value) {
const subscriptions = new Set();
const read = () => {
const running = context[context.length - 1];
if (running) subscribe(running, subscriptions);
return value;
};
const write = (nextValue) => {
value = nextValue;
for (const sub of [...subscriptions]) {
sub.execute();
}
};
return [read, write];
}Signal and deps tracking
function cleanup(running) {
for (const dep of running.dependencies) {
dep.delete(running);
}
running.dependencies.clear();
}
export function createEffect(fn) {
const running = {
execute,
dependencies: new Set()
};
const execute = () => {
cleanup(running);
context.push(running);
try {
fn();
} finally {
context.pop();
}
};
execute();
}
Reactions / Effects
console.log("1. Create Signal");
const [count, setCount] = createSignal(0);
console.log("2. Create Reaction");
createEffect(() => console.log("The count is", count()));
console.log("3. Set count to 5");
setCount(5);
console.log("4. Set count to 10");
setCount(10);export function createMemo(fn) {
const [s, set] = createSignal();
createEffect(() => set(fn()));
return s;
}Computed/memo
console.log("1. Create");
const [firstName, setFirstName] = createSignal("John");
const [lastName, setLastName] = createSignal("Smith");
const [showFullName, setShowFullName] = createSignal(true);
const displayName = createMemo(() => {
if (!showFullName()) return firstName();
return `${firstName()} ${lastName()}`
});
createEffect(() => console.log("My name is", displayName()));
console.log("2. Set showFullName: false ");
setShowFullName(false);
console.log("3. Change lastName");
setLastName("Legend");
console.log("4. Set showFullName: true");
setShowFullName(true);const [firstName, setFirstName] = createSignal("Salama");
const [lastName, setLastName] = createSignal("Ashoush");
const [showLastName, setShowLastName] = createSignal(false);
const fullName = createMemo(() => {
if(showLastName()){
return `${firstName()} ${lastName()}`;
}
return firstName();
});
createEffect(() => console.log(fullName())) // ?
setLastName("Salama")
setShowLastName(true)
setLastName("Ashoush")
Dynamic Tracking
state primitives
import { createStore } from "solid-js/store";
// stores are proxy based
const [person, setPerson] = createStore({
name: "Salama",
age: 30,
kids:[
{
name: "Omar",
age: 3,
},
{
name: "Ali",
age: 0.5,
},
]
});
// update a nested
setPerson('kids', 0, 'age', 2.5);
// there is also a mutuble version that allows this
const [person, setPerson] = createMutableStore({...})
kids[0].age = 2.5import { component$, useStore } from '@builder.io/qwik';
export default component$(() => {
const state = useStore({
name: "Salama",
age: 32,
kids:[
{
name: "Omar",
age: 5,
},
{
name: "Ali",
age: 2
},
]
});
return (
<>
<button onClick$={() => state.kids[0].age = 2.5}>Update nested</button>
<p>Count: {state.kids[0].age}</p>
</>
);
});
import { createResource } from 'solidjs';
// you can pass just a fetch function
const [data, { mutate, refetch }] = createResource(() => fetch("something"));
// or you can pass a signal to be tracked and the fetcher will run when it changes
const [data, { mutate, refetch }] = createResource(someSignal(), () => fetch("something"));
// access the data
data()
// update the data
mutate()
// rerun the fetcher funtion
refetch()export default component$(() => {
const prNumber = useSignal('3576');
const prTitle = useResource$<string>(async ({ track }) => {
// it will run first on mount (server), then re-run whenever prNumber changes (client)
// this means this code will run on the server and the browser
track(() => prNumber.value);
const response = await fetch(
`https://api.github.com/repos/BuilderIO/qwik/pulls/${prNumber.value}`
);
const data = await response.json();
return data.title as string;
});
return (
<>
<input type="number" bind:value={prNumber} />
<h1>PR#{prNumber}:</h1>
<Resource
value={prTitle}
onPending={() => <p>Loading...</p>}
onResolved={(title) => <h2>{title}</h2>}
/>
</>
);
});import { render } from "solid-js/web";
import { createSignal } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(1);
const increment = () => setCount(count() + 1);
// since the components only runs once this will lose reactivity
const doubleCount = count() * 2
return (
<button type="button" onClick={increment}>
{doubleCount}
</button>
);
}
render(() => <Counter />, document.getElementById("app")!);
import { render } from "solid-js/web";
import { createSignal, createMemo } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(1);
const increment = () => setCount(count() + 1);
// to fix this you have to ethier wrap it in a thunk
const doubleCount = () => count() * 2
// or createMemo
const doubleCount = createMemo(() => count() * 2)
return (
<button type="button" onClick={increment}>
{doubleCount()}
</button>
);
}
render(() => <Counter />, document.getElementById("app")!);
import { component$, useSignal } from '@builder.io/qwik';
// this all component will run once on the server and never on the client
// untill user interacts with the button
export default component$(() => {
// to achieve full resumeabilty qwik tie the reactive graph to the component graph
// which means you can't use signals outside of components
const count = useSignal(0);
// unlike solid this will stay reactive
// thanks to the qwik compiler
const doubleCount = count.value * 2
return (
<button onClick$={() => count.value++}>
Increment {count.value}
</button>
);
});import { useSignal, useComputed } from "@preact/signals";
function Counter() {
const count = useSignal(0);
const double = useComputed(() => count.value * 2);
return (
<div>
<p>{count} x 2 = {double}</p>
<button onClick={() => count.value++}>click me</button>
</div>
);
}// .svelte
<script>
// runes are like compiler macros that add lower level code on compile time
let a = $state(1);
let b = $state(2);
// computed/derived state, compiler will transform this to
// lower level signal primitives
let c = $derived(a + b)
b += 3
// there is also effects
effect$(() => {
console.log(c);
})
</script>
<p>{c}</p>
@Component({
signals: true,
selector: 'simple-counter',
template: `
<!-- count is invoked as a getter! -->
<p>Count {{ count() }}</p>
<p>{{ name }}</p> <!-- Not reactive! -->
<button (click)="increment()">Increment count</button>`,
})
export class SimpleCounter {
count = signal(0); // WritableSignal<number>
name = 'Morgan';
increment() {
this.count.update(c => c + 1);
}
}@Component({
signals: true,
selector: 'user-profile',
template: `
<p>Name: {{ firstName() }} {{ lastName() }}</p>
<p>Account suspended: {{ suspended() }}</p>
`,
})
export class UserProfile {
// Create an optional input without an initial value.
firstName = input<string>(); // Signal<string|undefined>
// Create an input with a default value
lastName = input('Smith'); // Signal<string>
// Create an input with options.
suspended = input<boolean>(false, {
alias: 'disabled',
}); // Signal<boolean>
}@Component({
signals: true,
selector: 'form-field',
template: `
<field-icon *ngFor="let icon of icons()"> {{ icon }} </field-icon>
<div class="focus-outline">
<input #field>
</div>
`
})
export class FormField {
icons = viewChildren(FieldIcon); // Signal<FieldIcon[]>
input = viewChild<ElementRef>('field'); // Signal<ElementRef>
someEventHandler() {
this.input().nativeElement.focus();
}
}use leptos::prelude::*;
#[component]
fn App() -> impl IntoView {
let (count, set_count) = signal(0);
view! {
<button
on:click=move |_| set_count.set(3)
>
"Click me: "
{count}
</button>
<p>
"Double count: "
{move || count.get() * 2}
</p>
}
}