Package Exports
- signal-utils
- signal-utils/-private/util
- signal-utils/array
- signal-utils/async-data
- signal-utils/async-function
- signal-utils/dedupe
- signal-utils/deep
- signal-utils/index
- signal-utils/local-copy
- signal-utils/map
- signal-utils/object
- signal-utils/promise
- signal-utils/set
- signal-utils/weak-map
- signal-utils/weak-set
Readme
signal-utils
Utils for the Signal's Proposal.
APIs
[!NOTE] All examples either use JavaScript or a mixed-language psuedocode[^syntax-based-off] to convey the reactive intention of using Signals. These utilities can be used in any framework that wires up Signals to their rendering implementation.
[^syntax-based-off]: The syntax is based of a mix of Glimmer-flavored Javascript and Svelte. The main thing being focused around JavaScript without having a custom file format. The <template>...</template> blocks may as well be HTML, and {{ }} escapes out to JS. I don't have a strong preference on {{ }} vs { }, the important thing is only to be consistent within an ecosystem.
@signal
A utility decorator for easily creating signals
import { signal } from 'signal-utils';
class State {
@signal accessor #value = 3;
get doubled() {
return this.#value * 2;
}
increment = () => this.#value++;
}
let state = new State();
// output: 6
// button clicked
// output: 8
<template>
<output>{{state.doubled}}</output>
<button onclick={{state.increment}}>+</button>
</template>Array
A reactive Array. This API mimics the built-in APIs and behaviors of Array.
import { SignalArray } from 'signal-utils/array';
let arr = new SignalArray([1, 2, 3]);
// output: 3
// button clicked
// output: 2
<template>
<output>{{arr.at(-1)}}</output>
<button onclick={{() => arr.pop()}}>pop</button>
</template>Other ways of constructing an array:
import { SignalArray, signalArray } from 'signal-utils/array';
SignalArray.from([1, 2, 3]);
signalArray([1, 2, 3]);Note that .from gives you more options of how to create your new array structure.
Object
A reactive Object. This API mimics the built-in APIs and behaviors of Object.
import { SignalObject } from 'signal-utils/object';
let obj = new SignalObject({
isLoading: true,
error: null,
result: null,
});
// output: true
// button clicked
// output: false
<template>
<output>{{obj.isLoading}}</output>
<button onclick={{() => obj.isLoading = false}}>finish</button>
</template>In this example, we could use a reactive object for quickly and dynamically creating an object of signals -- useful for when we don't know all the keys boforehand, or if we want a shorthand to creating many named signals.
Other ways of constructing an object:
import { SignalObject, signalObject } from 'signal-utils/object';
SignalObject.fromEntries([ /* ... */ ]);
signalObject({ /* ... */ } );Note that .fromEntries gives you more options of how to create your new object structure.
Map
wip
A reactive Map
WeakMap
wip
A reactive WeakMap
Set
wip
A reactive Set
WeakSet
wip
A reactive WeakSet
Promise (wrapper)
A reactive Promise handler that gives your reactive properties for when the promise resolves or rejects.
import { SignalAsyncData } from 'signal-utils/async-data';
const response = fetch('...');
const signalResponse = new SignalAsyncData(response);
// output: true
// after the fetch finishes
// output: false
<template>
<output>{{signalResponse.isLoading}}</output>
</template>There is also a load export which does the construction for you.
import { load } from 'signal-utils/async-data';
const response = fetch('...');
const signalResponse = load(response);
// output: true
// after the fetch finishes
// output: false
<template>
<output>{{signalResponse.isLoading}}</output>
</template>the signalResponse object has familiar properties on it:
valueerrorstateisResolvedisPendingisRejected
The important thing to note about using load / SignalAsyncData, is that you must already have a PromiseLike. For reactive-invocation of async functions, see the section below on signalFunction
async function
A reactive async function with pending/error state handling
import { Signal } from 'signal-polyfill';
import { signalFunction } from 'signal-utils/async-function';
const url = new Signal.State('...');
const signalResponse = signalFunction(async () => {
const response = await fetch(url.get()); // entangles with `url`
// after an away, you've detatched from the signal-auto-tracking
return response.json();
});
// output: true
// after the fetch finishes
// output: false
<template>
<output>{{signalResponse.isLoading}}</output>
</template>
the signalResponse object has familiar properties on it:
valueerrorstateisResolvedisPendingisRejectedisError(alias)isSettled(alias)isLoading(alias)isFinished(alias)retry()
localCopy + @localCopy
wip
utilities for the localCopy pattern
dedupe + @dedupe
wip
utilities for the dedupe pattern.
Contributing
Starting dev
pnpm install
pnpm startThis will start a concurrently command that runs the vite build and vitest tests in parallel.
Vitest isn't being used within the package, because we want to exercise the public API, generated types, etc (through package.json#exports and all that).
Credits and Inspiration
This library could not have been developed so quickly without borrowing from existing libraries that already built these patterns. This library, signal-utils, is an adaptation and aggregation of utilities found throughout the community.
tracked-built-insTrackedArrayfromtracked-built-insTrackedObjectfromtracked-built-insTrackedMapfromtracked-built-insTrackedWeakMapfromtracked-built-insTrackedSetfromtracked-built-insTrackedWeakSetfromtracked-built-ins
tracked-toolbox@dedupeTrackedfromtracked-toolbox@localCopyfromtracked-toolbox
ember-async-dataTrackedAsyncDatafromember-async-data
reactivewebtrackedFunctionfromreactiveweb