@signe/reactive
v2.8.3
Published
The `@signe/reactive` library is a lightweight and flexible library for creating reactive data structures in JavaScript. It provides utilities for creating reactive signals, computed properties, and effects, allowing you to build reactive applications wit
Readme
Reactive Library
What is @signe/reactive?
The @signe/reactive library is a lightweight and flexible library for creating reactive data structures in JavaScript. It provides utilities for creating reactive signals, computed properties, and effects, allowing you to build reactive applications with ease.
Installation
To install the @signe/reactive package, use the following command:
npm install @signe/reactiveUsage
Overview
The @signe/reactive library provides utilities for creating reactive signals, computed properties, and effects. It includes the following main functions:
- signal: Creates a reactive signal.
- computed: Creates a computed property that depends on other signals.
- linkedSignal: Creates a linked signal that can optionally support writing back to its dependencies.
- effect: Creates an effect that runs a function whenever the signals it depends on change.
- isSignal: Checks if a value is a signal.
1. signal
Creates a reactive signal which can hold any type of value (primitive, array, object, etc.).
Syntax
function signal<T>(defaultValue: T, options?: SignalOptions<T>): WritableSignal<T>;
interface SignalOptions<T> {
equal?: (a: T, b: T) => boolean;
}Example
import { signal } from '@signe/reactive';
// Primitive signal
const count = signal(0);
console.log(count()); // Output: 0
count.set(1);
console.log(count()); // Output: 1
// Array signal
const numbers = signal([1, 2, 3]);
numbers.mutate(arr => arr.push(4));
console.log(numbers()); // Output: [1, 2, 3, 4]
// Object signal
const person = signal({ name: 'John', age: 30 });
person.mutate(obj => obj.age = 31);
console.log(person()); // Output: { name: 'John', age: 31 }Signal Equality Functions
When creating a signal, you can optionally provide an equality function, which will be used to check whether the new value is actually different than the previous one. This prevents unnecessary emissions when setting the same value.
By default, signals use strict equality (===) for primitives and reference comparison for arrays and objects. With a custom equality function, you can define your own comparison logic.
Example 1: Deep equality for arrays
import { signal } from '@signe/reactive';
import _ from 'lodash';
const data = signal(['test'], { equal: _.isEqual });
const observedValues: any[] = [];
data.observable.subscribe(value => {
observedValues.push(value);
});
// Initial value is emitted
console.log(observedValues.length); // 1
// Set array with same content but different reference
data.set(['test']);
// Should not emit because content is equal (deep comparison)
console.log(observedValues.length); // Still 1
// Set array with different content
data.set(['different']);
console.log(observedValues.length); // 2Example 2: Deep equality for objects
import { signal } from '@signe/reactive';
// Deep equality function
const deepEqual = (a: any, b: any): boolean => {
if (a === b) return true;
if (a == null || b == null) return false;
if (typeof a !== 'object' || typeof b !== 'object') return false;
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
for (const key of keysA) {
if (!keysB.includes(key)) return false;
if (typeof a[key] === 'object' && a[key] !== null) {
if (!deepEqual(a[key], b[key])) return false;
} else if (a[key] !== b[key]) {
return false;
}
}
return true;
};
const objectSignal = signal({ a: 1, b: { c: 2 } }, { equal: deepEqual });
const observedValues: any[] = [];
objectSignal.observable.subscribe(value => {
observedValues.push(value);
});
// Initial value is emitted
const initialCount = observedValues.length;
// Set object with same content but different reference
objectSignal.set({ a: 1, b: { c: 2 } });
// Should not emit because content is equal (deep comparison)
console.log(observedValues.length === initialCount); // true
// Set object with different content
objectSignal.set({ a: 1, b: { c: 3 } });
console.log(observedValues.length === initialCount + 1); // trueExample 3: Approximate equality for numbers
import { signal } from '@signe/reactive';
// Custom equality that treats numbers within 0.1 as equal
const approximateEqual = (a: number, b: number): boolean => {
return Math.abs(a - b) < 0.1;
};
const numSignal = signal(5.0, { equal: approximateEqual });
const observedValues: number[] = [];
numSignal.observable.subscribe(value => {
observedValues.push(value);
});
// Initial value is emitted
const initialCount = observedValues.length;
// Set value within tolerance
numSignal.set(5.05);
// Should not emit because values are approximately equal
console.log(observedValues.length === initialCount); // true
// Set value outside tolerance
numSignal.set(5.2);
console.log(observedValues.length === initialCount + 1); // trueNote: When a custom equality function is provided, it completely replaces the default comparison logic. The signal will only emit when the equality function returns false (indicating the values are different).
2. computed
Creates a computed property that re-evaluates whenever the signals it depends on change.
Syntax
function computed<T>(computeFunction: () => T, disposableFn?: () => void): ComputedSignal<T>;Example
import { signal, computed } from '@signe/reactive';
const a = signal(1);
const b = signal(2);
const sum = computed(() => a() + b());
console.log(sum()); // Output: 3
a.set(2);
console.log(sum()); // Output: 43. linkedSignal
Creates a linked signal that depends on other signals. A linked signal automatically updates when its source changes, and can be overridden using set().
It supports two usage patterns:
- Simple computation:
linkedSignal(() => source() * 2)- can be overridden withset() - Source + Computation:
linkedSignal({ source: () => count(), computation: (value) => value * 2 })- allows access to previous values
Syntax
// Simple computation
function linkedSignal<T>(computation: () => T, options?: { equal?: (a: T, b: T) => boolean }): LinkedSignal<T>;
// Source + Computation
function linkedSignal<TSource, TValue>(options: {
source: Signal<TSource> | (() => TSource);
computation: (value: TSource, previous?: LinkedSignalPrevious<TSource, TValue>) => TValue;
equal?: (a: TValue, b: TValue) => boolean;
}): LinkedSignal<TValue>;Example 1: Simple computation
import { signal, linkedSignal } from '@signe/reactive';
const source = signal(0);
// linkedSignal depends on source and updates automatically
const linked = linkedSignal(() => source() * 2);
console.log(linked()); // 0
source.set(3);
console.log(linked()); // 6
linked.set(100); // Override the computed value
console.log(linked()); // 100Example 2: Source + Computation
import { signal, linkedSignal } from '@signe/reactive';
const count = signal(0);
const double = linkedSignal({
source: () => count(),
computation: (value) => value * 2
});
console.log(double()); // 0
count.set(5);
console.log(double()); // 10
double.set(50); // Override the computed value
console.log(double()); // 50Example 3: Source + Computation with previous values
import { signal, linkedSignal } from '@signe/reactive';
const shippingOptions = signal([
{ id: "1", name: "Standard" },
{ id: "2", name: "Express" },
]);
const selectedOption = linkedSignal<typeof shippingOptions extends signal<infer T> ? T : never, { id: string; name: string }>({
source: shippingOptions,
computation: (newOptions, previous) => {
// If the newOptions contain the previously selected option, preserve that selection.
// Otherwise, default to the first option.
return (
newOptions.find((opt) => opt.id === previous?.value.id) ?? newOptions[0]
);
},
});Example 4: Custom equality comparison
import { signal, linkedSignal } from '@signe/reactive';
const activeUser = signal({id: 123, name: 'Morgan', isAdmin: true});
const activeUserEditCopy = linkedSignal(() => activeUser(), {
// Consider the user as the same if it's the same `id`.
equal: (a, b) => a.id === b.id,
});
// Or, if separating `source` and `computation`
const activeUserEditCopy2 = linkedSignal({
source: activeUser,
computation: user => user,
equal: (a, b) => a.id === b.id,
});4. effect
Creates an effect that runs a function whenever the signals it depends on change. It can also return a cleanup function to run when the effect is disposed or re-run.
Syntax
function effect(fn: () => void): Effect;Example
import { signal, effect } from '@signe/reactive';
const count = signal(0);
const dispose = effect(() => {
console.log('Count changed:', count());
return () => console.log('Cleanup on count change');
});
count.set(1); // Output: "Count changed: 1"
dispose(); // Output: "Cleanup on count change"5. isSignal
Checks if a given value is a signal created by the signal function.
Syntax
function isSignal(value: any): boolean;Example
import { signal, isSignal } from '@signe/reactive';
const count = signal(0);
console.log(isSignal(count)); // Output: true
const notASignal = { value: 0 };
console.log(isSignal(notASignal)); // Output: falseAdvanced Examples
Working with ArraySubject
The ArraySubject class allows you to create signals specifically for arrays, tracking changes like additions, removals, and updates.
import { signal } from '@signe/reactive';
const arraySignal = signal([1, 2, 3]);
arraySignal.observable.subscribe((change) => {
console.log(change); // Outputs changes like { type: 'add', index: 3, items: [4] }
});
arraySignal.mutate(arr => arr.push(4));Working with ObjectSubject
The ObjectSubject class allows you to create signals specifically for objects, tracking changes like property additions, removals, and updates.
import { signal } from '@signe/reactive';
const objectSignal = signal({ a: 1, b: 2 });
objectSignal.observable.subscribe((change) => {
console.log(change); // Outputs changes like { type: 'update', key: 'a', value: 2 }
});
objectSignal.mutate(obj => obj.a = 2);Running Tests
To run the tests for @signe/reactive, use the following command:
npx vitestEnsure you have Vitest installed as a dev dependency:
npm install --save-dev vitest