Skip to content

Instantly share code, notes, and snippets.

@adnanalbeda
Last active January 28, 2025 10:05
Show Gist options
  • Save adnanalbeda/d2b260e47d0729af0da484412e468acc to your computer and use it in GitHub Desktop.
Save adnanalbeda/d2b260e47d0729af0da484412e468acc to your computer and use it in GitHub Desktop.
reactivity-vanillaJS
import { SignalGet, SignalUnsubscribe } from "./signal";
export type EffectDepsValues<T extends SignalGet<unknown>[]> = {
[k in keyof T]: T[k] extends SignalGet<infer R> ? R : never;
};
export type EffectCallback<
T extends [SignalGet<unknown>, ...SignalGet<unknown>[]]
> = (values: EffectDepsValues<T>) => unknown;
export type EffectCleanup = { (): void; isExecuted(): boolean };
export type CreateEffect = <
T extends [SignalGet<unknown>, ...SignalGet<unknown>[]]
>(
deps: T,
effect: EffectCallback<T>
) => EffectCleanup;
export function vEffect<
T extends [SignalGet<unknown>, ...SignalGet<unknown>[]]
>(deps: T, effect: EffectCallback<T>): EffectCleanup {
// to check if should fire
let watchingDeps: boolean | undefined = true;
// getters and their values
deps = Object.freeze(deps);
let values = Object.seal(deps.map((v) => v())) as EffectDepsValues<T>;
// build notifiers and store their cleanup function.
let notifiersCleanup: SignalUnsubscribe[] = deps.map((x, i) =>
x.subscribe((v) => {
if (!watchingDeps) return;
values[i] = v;
effect(values);
})
);
// cleanup function:
// 1- stop subscribed notifications from running effect.
// 2- run notifiers cleanup.
// 3- make resources undefined as they will no longer be used.
function cleanup() {
if (!watchingDeps)
throw new Error("Cannot run effect cleanup more than once.");
watchingDeps = undefined;
notifiersCleanup.forEach((x) => x());
notifiersCleanup = undefined as unknown as SignalUnsubscribe[];
deps = undefined as unknown as T;
values = undefined as unknown as EffectDepsValues<T>;
}
cleanup.isExecuted = () => !watchingDeps;
Object.freeze(cleanup);
return cleanup;
}
export type SignalGet<T> = {
(): T;
subscribe: SignalSubscribe<T>;
};
export type SignalSet<T> = {
(newValue: T): void;
};
export type SignalSubscribeNotifier<T> = {
(updatedValue: T): void;
};
export type SignalUnsubscribe = { (): void; isExecuted(): boolean };
export type SignalSubscribe<T> = {
(notifier: SignalSubscribeNotifier<T>): SignalUnsubscribe;
};
export type Signal<T> = [SignalGet<T>, SignalSet<T>, SignalSubscribe<T>];
export type CreateSignal<T> = {
(): Signal<T | undefined>;
(v: T): Signal<T>;
};
export function vSignal<T>(): Signal<T | undefined>;
export function vSignal<T>(initialValue: T): Signal<T>;
export function vSignal<T>(initialValue?: T) {
let value = initialValue as T;
// value get accessor
function get() {
return value;
}
// update value then notify subscribed notifiers of the change.
function set(newValue: T) {
if (value === newValue) return;
value = newValue as T;
notifySubscribedNotifiers(value);
}
// Value changed event callbacks
let subscribedNotifiers: SignalSubscribeNotifier<T>[] = [];
function notifySubscribedNotifiers(value: T) {
// notify (run) each subscribed value changed notifier (callback) about the new value.
subscribedNotifiers.forEach(async (x) => await x(value));
}
// add value notifier and returns its unsubscribe.
function subscribe(notifier: SignalSubscribeNotifier<T>): SignalUnsubscribe {
let watching: boolean | undefined = true;
subscribedNotifiers.push(notifier);
// remove notifiers from subscribed list of notifiers.
function unsubscribe() {
if (!watching)
throw new Error(
"Cannot run unsubscribe from this signal more than once."
);
watching = undefined;
// remove notifier from list of notifiers.
subscribedNotifiers = subscribedNotifiers.filter((v) => v !== notifier);
notifier = undefined as unknown as SignalSubscribeNotifier<T>; // cleanup callback
}
unsubscribe.isExecuted = () => !watching;
Object.freeze(unsubscribe);
return unsubscribe;
}
get.subscribe = subscribe;
return [get, set, subscribe];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment