JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 2829
  • Score
    100M100P100Q110351F
  • License MIT

Utils for use with the Signals Proposal: https://github.com/proposal-signals/proposal-signals

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:

  • value
  • error
  • state
  • isResolved
  • isPending
  • isRejected

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:

  • value
  • error
  • state
  • isResolved
  • isPending
  • isRejected
  • isError (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 start

This 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.