A tiny Observable implementation, the brilliant primitive you need to build a powerful reactive system.
npm install --save oby
The following functions are provided. They are just grouped and ordered alphabetically, the documentation for this library is fairly dry at the moment.
The following core functions are provided. These are functions which can't be implemented on top of the library itself, and on top of which everything else is constructed.
The main exported function wraps a value into an Observable, basically wrapping the value in a reactive shell.
An Observable is a function that works both as a getter and as a setter, and it can be writable or read-only, it has the following interface:
type Observable<T> = {
(): T,
( value: T ): T,
( fn: ( value: T ) => T ): T
};
type ObservableReadonly<T> = {
(): T
};
The $()
function has the following interface:
type ObservableOptions<T> = {
equals?: (( value: T, valuePrev: T ) => boolean) | false
};
function $ <T> (): Observable<T | undefined>;
function $ <T> ( value: undefined, options?: ObservableOptions<T | undefined> ): Observable<T | undefined>;
function $ <T> ( value: T, options?: ObservableOptions<T> ): Observable<T>;
This is how to use it:
import $ from 'oby';
// Create an Observable without an initial value
$<number> ();
// Create an Observable with an initial value
$(1);
// Create an Observable with an initial value and a custom equality function
const equals = ( value, valuePrev ) => Object.is ( value, valuePrev );
const o = $( 1, { equals } );
// Create an Observable with an initial value and a special "false" equality function, which is a shorthand for `() => false`, which causes the Observable to always emit when its setter is called
const oFalse = $( 1, { equals: false } );
// Getter
o (); // => 1
// Setter
o ( 2 ); // => 2
// Setter via a function, which gets called with the current value
o ( value => value + 1 ); // => 3
// Setter that sets a function, it has to be wrapped in another function because the above form exists
const noop = () => {};
o ( () => noop );
This function holds onto updates within its scope and flushes them out once it exits. It is useful as a performance optimizations when updating Observables multiple times whitin its scope while causing other Observables/computations/effects that depend on them to be re-evaluated only once.
- Note: Async batching is supported too, if the function that you pass to
$.batch
returns a promise that promise is awaited, and until it settles batching stays active.
Interface:
function batch <T> ( fn: () => T ): T;
Usage:
import $ from 'oby';
// Batch updates
const o = $(0);
$.batch ( () => {
o ( 1 );
o ( 2 );
o ( 3 );
o (); // => 0, updates have not been flushed out yet
});
o (); // => 3, now the latest update for this observable has been flushed out
This function allows you to register cleanup functions, which are executed automatically whenever the parent computation/effect/root is disposed of, which also happens before re-evaluating it.
Interface:
function cleanup ( fn: () => void ): void;
Usage:
import $ from 'oby';
// Attach some cleanup functions to a memo
const callback = $( () => {} );
$.memo ( () => {
const cb = callback ();
document.body.addEventListener ( 'click', cb );
$.cleanup ( () => {
document.body.removeEventListener ( 'click', cb );
});
$.cleanup ( () => { // You can have as many cleanup functions as you want
console.log ( 'cleaned up!' );
});
});
callback ( () => () => {} ); // Cleanups called and memo re-evaluated
This function provides a dependency injection mechanism, you can use it to register arbitrary values with the parent computation, and those values can be read, or overridden, at any point inside that computation.
Interface:
function context <T> ( symbol: symbol ): T | undefined;
function context <T> ( symbol: symbol, value: T ): undefined;
Usage:
import $ from 'oby';
// Reading and writing some values in the context
$.root ( () => {
const token = Symbol ( 'Some Context' );
$.context ( token, { foo: 123 } ); // Writing some context
$.effect ( () => {
const value = $.context ( token ); // Reading some context
console.log ( value.foo ); // => 123
$.effect ( () => {
$.context ( token, { foo: 321 } ); // Overriding some context
const value = $.context ( token ); // Reading again
console.log ( value.foo ); // => 321
});
});
});
An effect is similar to a memo, but it returns a function for manually disposing of it instead of an Observable, and if you return a function from inside it that's automatically registered as a cleanup function.
- Note: this function is inteded for side effects that interact with the outside world, if you need to derive some value out of some Observables or if you need to update Observables it's recommended to instead use
$.reaction
.
Interface:
function effect ( fn: () => (() => void) | void ): (() => void);
Usage:
import $ from 'oby';
// Create an effect with an automatically registered cleanup function
const callback = $( () => {} );
$.effect ( () => {
const cb = callback ();
document.body.addEventListener ( 'click', cb );
return () => { // Automatically-registered cleanup function
document.body.removeEventListener ( 'click', cb );
};
});
callback ( () => () => {} ); // Cleanups called and effect re-evaluated
This function allows you to register error handler functions, which are executed automatically whenever the parent computation/effect/root throws. If any error handlers are present the error is caught automatically and is passed to error handlers. Errors bubble up across computations.
Remember to register your error handlers before doing anything else, or the computation may throw before error handlers are registered, in which case you won't be able to catch the error with that.
Interface:
function error ( fn: ( error: Error ) => void ): void;
Usage:
import $ from 'oby';
// Register an error handler function to a memo
const o = $( 0 );
$.memo ( () => {
$.error ( error => {
console.log ( 'Error caught!' );
console.log ( error );
});
if ( o () === 2 ) {
throw new Error ( 'Some error' );
}
});
o ( 1 ); // No error is thrown, error handlers are not called
o ( 2 ); // An error is thrown, so it's caught and passed to the registered error handlers
This function tells you if batching is currently active, which could be especially useful to know when batching asynchronous functions.
Interface:
function isBatching (): boolean;
Usage:
import $ from 'oby';
// Checking if currently batching
$.isBatching (); // => false
$.batch ( () => {
$.isBatching (); // => true
});
$.isBatching (); // => false
This function allows you to tell apart Observables from other values.
Interface:
function isObservable <T = unknown> ( value: unknown ): value is Observable<T> | ObservableReadonly<T>;
Usage:
import $ from 'oby';
// Checking
$.isObservable ( $() ); // => true
$.isObservable ( {} ); // => false
This function allows you to tell apart Stores from other values.
Interface:
function isStore ( value: unknown ): boolean;
Usage:
import $ from 'oby';
// Checking
$.isStore ( $.store ( {} ) ); // => true
$.isStore ( {} ); // => false
This is the function where the magic happens, it generates a new read-only Observable with the result of the function passed to it, the function is automatically re-executed whenever it's dependencies change, and dependencies are tracked automatically.
Usually you can just pass a plain function around, if that's the case the only thing you'll get out of $.memo
is memoization, which is a performance optimization, hence the name.
There are no restrictions, you can nest these freely, create new Observables inside them, whatever you want.
- Note: the Observable returned by a memo could always potentially resolve to
undefined
if an error is thrown inside the function but it's caught by an error handler inside it. In that scenario you should account forundefined
in the return type yourself. - Note: this function should not have side effects, the function is expected to be pure, for side effects you should use
$.effect
. - Note: if you want to update some Observables inside a computation you should probably first of all try to avoid doing that in the first place if you can, but if you can't avoid it you should use
$.reaction
for that instead.
Interface:
function memo <T> ( fn: () => T, options?: ObservableOptions<T | undefined> ): ObservableReadonly<T>;
Usage:
import $ from 'oby';
// Make a new memoized Observable
const a = $(1);
const b = $(2);
const c = $(3);
const sum = $.memo ( () => {
return a () + b () + c ();
});
sum (); // => 6
a ( 2 );
sum (); // => 7
b ( 3 );
sum (); // => 8
c ( 4 );
sum (); // => 9
This function allows you to register a listener for a single Observable, directly, without using wrapper memos/effects/reactions.
- Note: this is an advanced function intended mostly for internal usage.
Interface:
function on <T> ( observable: Observable<T> | ObservableReadonly<T>, listener: (( value: T, valuePrev?: T ) => void) ): (() => void);
Usage:
import $ from 'oby';
// Register a listener for an Observable
const o = $(0);
$.on ( o, ( value, valuePrev ) => {
console.log ( value, valuePrev ); // This function is called immediately upon registration
});
o ( 1 ); // Changing the value, causing the listener to be called again
This function allows you to unregister a previously registered listener from a single Observable.
- Note: this is an advanced function intended mostly for internal usage.
Interface:
function off <T> ( observable: Observable<T> | ObservableReadonly<T>, listener: (( value: T, valuePrev?: T ) => void) ): void;
Usage:
import $ from 'oby';
// Unregistering a listener for an Observable
const o = $(0);
const onChange = ( value, valuePrev ) => console.log ( value, valuePrev );
$.on ( o, onChange ); // Registering
$.off ( o, onChange ); // Unregistering
o ( 1 ); // Listener not called, because it got unregistered
This function tells you some metadata about the current owner/observer. There's always an owner.
- Note: this is an advanced function intended mostly for internal usage, you almost certainly don't have a use case for using this function.
Interface:
type Owner = {
isSuperRoot: boolean, // This tells you if the nearest owner of your current code is a super root, which is kind of a default root that everything gets wrapped with
isRoot: boolean, // This tells you if the nearest owner of your current code is a root
isSuspense: boolean, // This tells you if the nearest owner of your current code is a suspense
isComputation: boolean // This tells you if the nearest owner of your current code is an effect or a memo or a reaction
};
function owner (): Owner;
Usage:
import $ from 'oby';
// Check if you are right below the super root
$.owner ().isSuperRoot; // => true
$.effect ( () => {
$.owner ().isSuperRoot; // => false
});
A reaction is similar to an effect, except that it's not affected by suspense.
- Note: this is an advanced function intended mostly for internal usage, you'd almost always want to simply either use a memo or an effect.
Interface:
function reaction ( fn: () => (() => void) | void ): (() => void);
Usage:
import $ from 'oby';
// Create a reaction with an automatically registered cleanup function
const callback = $( () => {} );
$.reaction ( () => {
const cb = callback ();
document.body.addEventListener ( 'click', cb );
return () => { // Automatically-registered cleanup function
document.body.removeEventListener ( 'click', cb );
};
});
callback ( () => () => {} ); // Cleanups called and reaction re-evaluated
This function creates a computation root, computation roots are detached from parent roots/memos/effects and will outlive them, so they must be manually disposed of, disposing them ends all the reactvity inside them, except for any eventual nested root. The value returned by the function is returned by the root itself.
- Note: the value returned by the root could always potentially resolve to
undefined
if an error is thrown inside the function but it's caught by an error handler inside it. In that scenario you should account forundefined
in the return type yourself.
Interface:
function root <T> ( fn: ( dispose: () => void ) => T ): T;
Usage:
import $ from 'oby';
// Create a root and dispose of it
$.root ( dispose => {
let calls = 0;
const a = $(0);
const b = $.memo ( () => {
calls += 1;
return a ();
});
console.log ( calls ); // => 1
b (); // => 0
a ( 1 );
console.log ( calls ); // => 2
b (); // => 1
dispose (); // Now all the reactivity inside this root stops
a ( 2 );
console.log ( calls ); // => 2
b (); // => 1
});
This function returns a deeply reactive version of the passed object, where property accesses and writes are automatically interpreted as Observables reads and writes for you.
You can just use the reactive object like you would with a regular non-reactive one, every aspect of the reactivity is handled for you under the hood, just remember to perform reads in a computation if you want to subscribe to them.
-
Note: Only the following types of values will be handled automatically by the reactivity system: plain objects, plain arrays, primitives.
-
Note: Assignments to the following properties won't be reactive, as making those reactive would have more cons than pros:
__proto__
,prototype
,constructor
,hasOwnProperty
,isPrototypeOf
,propertyIsEnumerable
,toLocaleString
,toSource
,toString
,valueOf
, allArray
methods. -
Note: Getters and setters that are properties of arrays, if for whatever reason you have those, won't be reactive.
-
Note: Getters and setters that are assigned to symbols, if for whatever reason you have those, won't be reactive.
-
Note: A powerful function is provided,
$.store.on
, for listening to any changes happening inside a store. Changes are batched automatically within a microtask for you. If you use this function it's advisable to not have multiple instances of the same object inside a single store, or you may hit some edge cases where a listener doesn't fire because another path where the same object is available, and where it was edited from, hasn't been discovered yet, since discovery is lazy as otherwise it would be expensive. -
Note: A powerful function is provided,
$.store.reoncile
, that basically merges the content of the second argument into the first one, preserving wrapper objects in the first argument as much as possible, which can avoid many unnecessary re-renderings down the line. Currently getters/setters/symbols from the second argument are ignored, as supporting those would make this function significantly slower, and you most probably don't need them anyway if you are using this function.
Interface:
type StoreListenableTarget = Record<string | number | symbol, any> | (() => any);
type StoreReconcileableTarget = Record<string | number | symbol, any> | Array<any>;
type StoreOptions = {
equals?: (( value: unknown, valuePrev: unknown ) => boolean) | false
};
function store <T> ( value: T, options?: StoreOptions ): T;
store.on = function on ( target: ArrayMaybe<StoreListenableTarget>, callback: () => void ): (() => void);
store.reconcile = function reconcile <T extends StoreReconcileableTarget> ( prev: T, next: T ): T;
store.untrack = function untrack <T> ( value: T ): T;
store.unwrap = function unwrap <T> ( value: T ): T;
Usage:
import $ from 'oby';
// Make a reactive plain object
const obj = $.store ({ foo: { deep: 123 } });
$.effect ( () => {
obj.foo.deep; // Subscribe to "foo" and "foo.deep"
});
obj.foo.deep = 321; // Cause the effect to re-run
// Make a reactive array
const arr = $.store ([ 1, 2, 3 ]);
$.effect ( () => {
arr.forEach ( value => { // Subscribe to the entire array
console.log ( value );
});
});
arr.push ( 123 ); // Cause the effect to re-run
// Make a reactive object, with a custom equality function, which is inherited by children also
const equals = ( value, valuePrev ) => Object.is ( value, valuePrev );
const eobj = $.store ( { some: { arbitrary: { velue: true } } }, { equals } );
// Untrack parts of a store, bailing out of automatic proxying
const uobj =
BE96
$.store ({
foo: {} // This object will become a store automatically
bar: $.store.untrack ( {} ) // This object will stay a plain object
});
// Get a non-reactive object out of a reactive one
const pobj = $.store.unwrap ( obj );
// Get a non-reactive array out of a reactive one
const parr = $.store.unwrap ( arr );
// Reconcile a store with new data
const rec = $.store ({ foo: { deep: { value: 123, other: '123' } } });
const dataNext = { foo: { deep: { value: 321, other: '321' } } };
$.store.reconcile ( rec, dataNext ); // Now "rec" contains the data from "dataNext", but none of its internal objects, in this case, got deleted or created, they just got updated
// Listen for changes inside a store using a selector, necessary if you want to listen to a primitive
$.store.on ( () => obj.foo.deep, () => {
console.log ( '"obj.foo.deep" changed!' );
});
// Listen for changes inside a whole store
$.store.on ( obj, () => {
console.log ( 'Something inside "obj" changed!' );
});
// Listen for changes inside a whole sub-store, which is just another store created automatically for you really
$.store.on ( obj.foo, () => {
console.log ( 'Something inside "obj.foo" changed!' );
});
// Listen for changes inside multiple targets, the callback will still be fired once if multiple targets are edited within the same microtask
$.store.on ( [obj, arr], () => {
console.log ( 'Something inside "obj" and/or "arr" changed!' );
});
This function allows for reading Observables without creating dependencies on them, temporarily turning off tracking basically.
Interface:
function untrack <T> ( fn: () => T ): T;
function untrack <T> ( value: T ): T;
Usage:
import $ from 'oby';
// Untracking a single Observable
const o = $(0);
$.untrack ( o ); // => 0
// Untracking multiple Observables
const a = $(1);
const b = $(2);
const c = $(3);
const sum = $.untrack ( () => {
return a () + b () + c ();
});
console.log ( sum ); // => 6
a ( 2 );
b ( 3 );
c ( 4 );
console.log ( sum ); // => 6, it's just a value, not a reactive Observable
// Untracking a non function, it's just returned as is
$.untrack ( 123 ); // => 123
This function allows you to create a function for executing code as if it was a child of the computation active when the function was originally created.
- Note: this is an advanced function intended mostly for internal usage.
Interface:
function with (): (<T> ( fn: () => T ): T);
Usage:
import $ from 'oby';
// Reading some values from the context as if the code was executing inside a different computation
$.root ( () => {
const token = Symbol ( 'Some Context' );
$.context ( token, { foo: 123 } ); // Writing some context
const runWithRoot = $.with ();
$.effect ( () => {
$.context ( token, { foo: 321 } ); // Overriding some context
const value = $.context ( token ); // Reading the context
console.log ( value.foo ); // => 321
runWithRoot ( () => {
const value = $.context ( token ); // Reading the context
console.log ( value.foo ); // => 123
});
});
});
The following flow functions are provided. These functions are like the reactive versions of native constructs in the language.
This is the reactive version of the native if
statement. It returns a read-only Observable that resolves to the passed value if the condition is truthy, or to the optional fallback otherwise.
Interface:
function if <T, F> ( when: (() => boolean) | boolean, valueTrue: T, valueFalse?: F ): ObservableReadonly<T | F | undefined>;
Usage:
import $ from 'oby';
// Toggling an if
const bool = $(false);
const result = $.if ( bool, 123, 321 );
result (); // => 321
bool ( true );
result (); // => 123
This is the reactive version of the native Array.prototype.map
, it maps over an array of values while caching results for values that didn't change.
This is recommended over $.forIndex
if the array contains no duplicates. It will still work with duplicates, but performance will degrade.
Interface:
function for <T, R, F> ( values: (() => readonly T[]) | readonly T[], fn: (( value: T, index: ObservableReadonly<number> ) => R), fallback: F | [] = [] ): ObservableReadonly<R[] | F>;
Usage:
import $ from 'oby';
// Map over an array of values
const o1 = $(1);
const o2 = $(2);
const os = $([ o1, o2 ]);
const mapped = $.for ( os, o => {
return someExpensiveFunction ( o () );
});
// Update the "mapped" Observable
os ([ o1, o2, o3 ]);
This is an alternative reactive version of the native Array.prototype.map
, it maps over an array of values while caching results for values whose position in the array didn't change.
This is an alternative to $.for
that uses the index of the value in the array for caching, rather than the value itself.
It's recommended to use $.forIndex
for arrays containing duplicate values and/or arrays containing primitive values, and $.for
for everything else.
The passed function will always be called with a read-only Observable containing the current value at the index being mapped.
Interface:
type Value<T = unknown> = T extends ObservableReadonly<infer U> ? ObservableReadonly<U> : ObservableReadonly<T>;
function forIndex <T, R, F> ( values: (() => readonly T[]) | readonly T[], fn: (( value: Value<T>, index: number ) => R), fallback: F | [] = [] ): ObservableReadonly<R[] | F>;
Usage:
import $ from 'oby';
// Map over an array of primitive values, containing duplicates
const values = $([ 1, 1, 2, 2, 3 ]);
const mapped = $.for ( values, value => {
return $.memo ( () => { // Wrapping in a memo to listen to changes in "value"
return `Double: ${value () ** 2}`;
});
});
// Update the values Observable
values ([ 1, 2, 2 ]); // Only the value at index "1" changed, so only the mapped value for that will be refreshed
This is an alternative reactive version of the native Array.prototype.map
, it maps over an array of values while caching results for values that didn't change, and repurposing computations for items that got discarded for new items that need to be mapped.
This is an alternative to $.for
and $.forIndex
that enables reusing the same computation for different items, when possible. Reusing the same computation means also reusing everything in it, which could mean expensive DOM nodes to generate, or something else.
Basically Array.prototype.map
doesn't wrap the value nor the index in an observable, $.for
wraps the index only in an observable, $.forIndex
wraps the value only in an observable, and $.forValue
wraps both the value and the index in observables.
This is useful for use cases like virtualized rendering, where $.for
would cause some nodes to be discarded and others to be created, $.forIndex
would cause all nodes to be repurposed, but $.forValue
allows you to only repurpose the nodes that would have been discareded by $.for
, not all of them.
This is a more advanced method, it's recommended to simply use $.for
or $.forIndex
, until you really understand how to squeeze extra performance with this, and you actually need that performance.
Interface:
type Value<T = unknown> = T extends ObservableReadonly<infer U> ? ObservableReadonly<U> : ObservableReadonly<T>;
function forValue <T, R, F> ( values: (() => readonly T[]) | readonly T[], fn: (( value: Value<T>, index: ObservableReadonly<number> ) => R), fallback: F | [] = [] ): ObservableReadonly<R[] | F>;
Usage:
import $ from 'oby';
// Map over an array of primitive values
const values = $([ 1, 2, 3 ]);
const mapped = $.forValue ( values, value => {
return $.memo ( () => { // Wrapping in a memo to listen to changes in "value"
return `Double: ${value () ** 2}`;
});
});
// Update the values Observable
values ([ 1, 4, 2 ]); // Now the computation that handled `3` will receive `4` as the new value of the "value" observable, the old memo is not disposed and a new one is not created, the old one is simply refreshed because one of its dependencies changed
This function allows you to recursively pause and resume the execution of all, current and future, effects created inside it.
This is very useful in some scenarios, for example you may want to keep a particular branch of computation around, if it'd be expensive to dispose of it and re-create it again, but you don't want its effects to be executing as they would probably interact with the rest of your application.
A parent suspense boundary will also recursively pause children suspense boundaries.
Interface:
function suspense <T> ( suspended: FunctionMaybe<unknown>, fn: () => T ): T;
Usage:
import $ from 'oby';
// Create a suspendable branch of computation
const title = $('Some Title');
const suspended = $(false);
$.suspense ( suspended, () => {
$.effect ( () => {
document.title = title (); // Changing something in the outside world, in other words performing a side effect
});
});
// Pausing effects inside the suspense boundary
suspended ( true );
title ( 'Some Other Title' ); // This won't cause the effect to be re-executed, since it's paused
// Resuming effects inside the suspense boundary
suspended ( false ); // This will cause the effect to be re-executed, as it had pending updates
This is the reactive version of the native switch
statement. It returns a read-only Observable that resolves to the value of the first matching case, or the value of the default condition, or undefined otherwise.
Interface:
type SwitchCase<T, R> = [T, R];
type SwitchDefault<R> = [R];
type SwitchValue<T, R> = SwitchCase<T, R> | SwitchDefault<R>;
function switch <T, R, F> ( when: (() => T) | T, values: SwitchValue<T, R>[], fallback?: F ): ObservableReadonly<R | F | undefined>;
Usage:
import $ from 'oby';
// Switching cases
const o = $(1);
const result = $.switch ( o, [[1, '1'], [2, '2'], [1, '1.1'], ['default']] );
result (); // => '1'
o ( 2 );
result (); // => '2'
o ( 3 );
result (); // => 'default'
This is the reactive version of the native ternary operator. It returns a read-only Observable that resolves to the first value if the condition is truthy, or the second value otherwise.
Interface:
function ternary <T, F> ( when: (() => boolean) | boolean, valueTrue: T, valueFalse: T ): ObservableReadonly<T | F>;
Usage:
import $ from 'oby';
// Toggling an ternary
const bool = $(false);
const result = $.ternary ( bool, 123, 321 );
result (); // => 321
bool ( true );
result (); // => 123
This is the reactive version of the native try..catch
block. If no errors happen the regular value function is executed, otherwise the fallback function is executed, whatever they return is returned wrapped in a read-only Observable.
This is also commonly referred to as an "error boundary".
Interface:
function tryCatch <T, F> ( value: T, catchFn: ({ error, reset }: { error: Error, reset: () => void }) => F ): ObservableReadonly<T | F>;
Usage:
import $ from 'oby';
// Create an tryCatch boundary
const o = $(false);
const fallback = ({ error, reset }) => {
console.log ( error );
setTimeout ( () => { // Attempting to recovering after 1s
o ( false );
reset ();
}, 1000 );
return 'fallback!';
};
const regular = () => {
if ( o () ) throw 'whoops!';
return 'regular!';
};
const result = $.tryCatch ( fallback, regular );
result (); // => 'regular!'
// Cause an error to be thrown inside the boundary
o ( true );
result (); // => 'fallback!'
The following utilities functions are provided. These functions are either simple to implement and pretty handy, or pretty useful in edge scenarios and hard to implement, so they are provided for you.
This function is like the reactive equivalent of the !!
operator, it returns you a boolean, or a function to a boolean, depending on the input that you give it.
Interface:
function boolean ( value: FunctionMaybe<unknown> ): FunctionMaybe<boolean>;
Usage:
import $ from 'oby';
// Implementing a custom if function
function if ( when: FunctionMaybe<unknown>, whenTrue: FunctionMaybe<unknown>, whenFalse: FunctionMaybe<unknown> ) {
const condition = $.boolean ( when );
return $.memo ( () => {
return $.resolve ( $.get ( condition ) ? whenTrue : whenFalse );
});
}
This function returns a read-only Observable that tells you if the parent computation got disposed of or not.
Interface:
function disposed (): ObservableReadonly<boolean>;
Usage:
import $ from 'oby';
// Create an effect whose function knows when it's disposed
const url = $( 'htts://my.api' );
$.effect ( () => {
const disposed = $.disposed ();
const onResolve = ( response: Response ): void => {
if ( disposed () ) return; // The effect got disposed, no need to handle the response anymore
// Do something with the response
};
const onReject = ( error: unknown ): void => {
if ( disposed () ) return; // The effect got disposed, no need to handle the error anymore
// Do something with the error
};
fetch ( url () ).then ( onResolve, onReject );
});
url ( 'https://my.api2' ); // This causes the effect to be re-executed, and the previous `disposed` Observable will be set to `true`
This function gets the value out of something, if it gets passed an Observable or a function then by default it calls it, otherwise it just returns the value. You can also opt-out of calling plain functions, which is useful when dealing with callbacks.
Interface:
function get <T> ( value: T, getFunction?: true ): (T extends (() => infer U) ? U : T);
function get <T> ( value: T, getFunction: false ): (T extends ObservableReadonly<infer U> ? U : T);
Usage:
import $ from 'oby';
// Getting the value out of an Observable
const o = $(123);
$.get ( o ); // => 123
// Getting the value out of a function
$.get ( () => 123 ); // => 123
// Getting the value out of an Observable but not out of a function
$.get ( o, false ); // => 123
$.get ( () => 123, false ); // => () => 123
// Getting the value out of a non-Observable and non-function
$.get ( 123 ); // => 123
This function makes a read-only Observable out of any Observable you pass it. It's useful when you want to pass an Observable around but you want to be sure that they can't change it's value but only read it.
Interface:
function readonly <T> ( observable: Observable<T> | ObservableReadonly<T> ): ObservableReadonly<T>;
Usage:
import $ from 'oby';
// Making a read-only Observable
const o = $(123);
const ro = $.readonly ( o );
// Getting
ro (); // => 123
// Setting throws
ro ( 321 ); // An error will be thrown, read-only Observables can't be set
This function recursively resolves reactivity in the passed value. Basically it replaces each function it can find with the result of $.memo ( () => $.resolve ( fn () ) )
.
You may never need to use this function yourself, but it's necessary internally at times to make sure that a child value is properly tracked by its parent computation.
This function is used internally by $.if
, $.for
, $.switch
, $.ternary
, $.tryCatch
, as they need to resolve values to make sure the memo they give you can properly keep track of dependencies.
Interface:
type ResolvablePrimitive = null | undefined | boolean | number | bigint | string | symbol;
type ResolvableArray = Resolvable[];
type ResolvableObject = { [Key in string | number | symbol]?: Resolvable };
type ResolvableFunction = () => Resolvable;
type Resolvable = ResolvablePrimitive | ResolvableObject | ResolvableArray | ResolvableFunction;
const resolve = <T> ( value: T ): T extends Resolvable ? T : never;
Usage:
import $ from 'oby';
// Resolve a plain value
$.resolve ( 123 ); // => 123
// Resolve a function
$.resolve ( () => 123 ); // => ObservableReadonly<123>
// Resolve a nested function
$.resolve ( () => () => 123 ); // => ObservableReadonly<ObservableReadonly<123>>
// Resolve a plain array
$.resolve ( [123] ); // => [123]
// Resolve an array containing a function
$.resolve ( [() => 123] ); // => [ObservableReadonly<123>]
// Resolve an array containing arrays and functions
$.resolve ( [() => 123, [() => [() => 123]]] ); // => [ObservableReadonly<123>, [ObservableReadonly<[ObservableReadonly<123>]>]]
// Resolve a plain object
$.resolve ( { foo: 123 } ); // => { foo: 123 }
// Resolve a plain object containing a function, plain objects are simply returned as is
$.resolve ( { foo: () => 123 } ); // => { foo: () => 123 }
This function is useful for optimizing performance when you need to, for example, know when an item within a set is the selected one.
If you use this function then when a new item should be the selected one the old one is unselected, and the new one is selected, directly, without checking if each element in the set is the currently selected one. This turns a O(n)
operation into an O(2)
one.
Interface:
type SelectorFunction<T> = ( value: T ) => ObservableReadonly<boolean>;
function selector <T> ( source: () => T ): SelectorFunction<T>;
Usage:
import $ from 'oby';
// Making a selector
const values = [1, 2, 3, 4, 5];
const selected = $(-1);
const select = value => selected ( value );
const selector = $.selector ( selected );
values.forEach ( value => {
$.effect ( () => {
const selected = selector ( value );
if ( selected () ) return;
console.log ( `${value} selected!` );
});
});
select ( 1 ); // It causes only 2 effect to re-execute, not 5 or however many there are
select ( 5 ); // It causes only 2 effect to re-execute, not 5 or however many there are
The following T 7B49 ypeScript types are provided.
This type describes a regular writable Observable, like what you'd get from $()
.
Interface:
type Observable<T> = {
(): T,
( value: T ): T,
( fn: ( value: T ) => T ): T
};
This type describes a read-only Observable, like what you'd get from $.memo
or $.readonly
.
Interface:
type ObservableReadonly<T> = {
(): T
};
This type describes the options object that various functions can accept to tweak how the underlying Observable works.
Interface:
type ObservableOptions<T> = {
equals?: (( value: T, valuePrev: T ) => boolean) | false
};
This type describes the options object that the $.store
function can accept.
Interface:
type StoreOptions = {
equals?: (( value: unknown, valuePrev: unknown ) => boolean) | false
};
- S: for paving the way to this awesome reactive way of writing software.
- sinuous/observable: for making me fall in love with Observables and providing a good implementation that this library was originally based on.
- solid: for being a great sort of reference implementation, popularizing Signal-based reactivity, and having built a great community.
- trkl: for being so inspiringly small.
MIT © Fabio Spampinato