SkillAgentSearch skills...

Signals

A tiny (~1kB minzipped) and extremely fast library for creating reactive observables via functions.

Install / Use

/learn @maverick-js/Signals
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Signals

[![package-badge]][package] [![license-badge]][license]

🏆 The goal of this library is to provide a lightweight reactivity API for other UI libraries to be built on top of. It follows the "lazy principle" that Svelte adheres to - don't do any unnecessary work and don't place the burden of figuring it out on the developer.

This is a tiny (~1kB minzipped) library for creating reactive observables via functions called signals. You can use signals to store state, create computed properties (y = mx + b), and subscribe to updates as its value changes.

  • 🪶 Light (~1kB minzipped)
  • 💽 Works in both browsers and Node.js
  • 🌎 All types are observable (i.e., string, array, object, etc.)
  • 🕵️‍♀️ Only updates when value has changed
  • ⏱️ Batched updates via microtask scheduler
  • 😴 Lazy by default - efficiently re-computes only what's needed
  • 🔬 Computations via computed
  • 📞 Effect subscriptions via effect
  • 🐛 Debugging identifiers
  • 💪 Strongly typed - built with TypeScript

⏭️ Skip to API

⏭️ Skip to TypeScript

⏭️ Skip to Benchmarks

Here's a simple demo to see how it works:

[Open in StackBlitz][stackblitz-demo]

import { root, signal, computed, effect, tick } from '@maverick-js/signals';

root((dispose) => {
  // Create - all types supported (string, array, object, etc.)
  const $m = signal(1);
  const $x = signal(1);
  const $b = signal(0);

  // Compute - only re-computed when `$m`, `$x`, or `$b` changes.
  const $y = computed(() => $m() * $x() + $b());

  // Effect - this will run whenever `$y` is updated.
  const stop = effect(() => {
    console.log($y());

    // Called each time `effect` ends and when finally disposed.
    return () => {};
  });

  $m.set(10); // logs `10` inside effect

  // Flush queue synchronously so effect is run.
  // Otherwise, effects will be batched and run on the microtask queue.
  tick();

  $b.set((prev) => prev + 5); // logs `15` inside effect

  tick();

  // Nothing has changed - no re-compute.
  $y();

  // Stop running effect.
  stop();

  // ...

  // Dispose of all signals inside `root`.
  dispose();
});

Installation

$: npm i @maverick-js/signals

$: pnpm i @maverick-js/signals

$: yarn add @maverick-js/signals

API

root

Computations are generally child computations. When their respective parent scope is destroyed so are they. You can create orphan computations (i.e., no parent). Orphans will live in memory until their internal object references are garbage collected (GC) (i.e., dropped from memory):

import { computed } from '@maverick-js/signals';

const obj = {};

// This is an orphan - GC'd when `obj` is.
const $b = computed(() => obj);

Orphans can make it hard to determine when a computation is disposed so you'll generally want to ensure you only create child computations. The root function stores all inner computations as a child and provides a function to easily dispose of them all:

import { root, signal, computed, effect } from '@maverick-js/signals';

root((dispose) => {
  const $a = signal(10);
  const $b = computed(() => $a());

  effect(() => console.log($b()));

  // Disposes of `$a`, $b`, and `effect`.
  dispose();
});
// `root` returns the result of the given function.
const result = root(() => 10);

console.log(result); // logs `10`

signal

Wraps the given value into a signal. The signal will return the current value when invoked fn(), and provide a simple write API via set(). The value can now be observed when used inside other computations created with computed and effect.

import { signal } from '@maverick-js/signals';

const $a = signal(10);

$a(); // read
$a.set(20); // write (1)
$a.set((prev) => prev + 10); // write (2)

Warning Read the tick section below to understand batched updates.

computed

Creates a new signal whose value is computed and returned by the given function. The given compute function is only re-run when one of its dependencies are updated. Dependencies are are all signals that are read during execution.

import { signal, computed, tick } from '@maverick-js/signals';

const $a = signal(10);
const $b = signal(10);
const $c = computed(() => $a() + $b());

console.log($c()); // logs 20

$a.set(20);
tick();
console.log($c()); // logs 30

$b.set(20);
tick();
console.log($c()); // logs 40

// Nothing changed - no re-compute.
console.log($c()); // logs 40
import { signal, computed } from '@maverick-js/signals';

const $a = signal(10);
const $b = signal(10);
const $c = computed(() => $a() + $b());

// Computed signals can be deeply nested.
const $d = computed(() => $a() + $b() + $c());
const $e = computed(() => $d());

effect

Invokes the given function each time any of the signals that are read inside are updated (i.e., their value changes). The effect is immediately invoked on initialization.

import { signal, computed, effect } from '@maverick-js/signals';

const $a = signal(10);
const $b = signal(20);
const $c = computed(() => $a() + $b());

// This effect will run each time `$a` or `$b` is updated.
const stop = effect(() => console.log($c()));

// Stop observing.
stop();

You can optionally return a function from inside the effect that will be run each time the effect re-runs and when it's finally stopped/disposed of:

effect(() => {
  return () => {
    // Called each time effect re-runs and when disposed of.
  };
});

peek

Returns the current value stored inside the given compute function whilst disabling observer tracking, i.e. without triggering any dependencies. Use untrack if you want to also disable scope tracking.

import { signal, computed, peek } from '@maverick-js/signals';

const $a = signal(10);

const $b = computed(() => {
  // `$a` will not trigger updates on `$b`.
  const value = peek($a);
});

untrack

Returns the current value inside a signal whilst disabling both scope and observer tracking. Use peek if only observer tracking should be disabled.

import { signal, effect, untrack } from '@maverick-js/signals';

effect(() => {
  untrack(() => {
    // `$a` is now an orphan and also not tracked by the outer effect.
    const $a = signal(10);
  });
});

readonly

Takes in the given signal and makes it read only by removing access to write operations (i.e., set()).

import { signal, readonly } from '@maverick-js/signals';

const $a = signal(10);
const $b = readonly($a);

console.log($b()); // logs 10

// We can still update value through `$a`.
$a.set(20);

console.log($b()); // logs 20

tick

By default, signal updates are batched on the microtask queue which is an async process. You can flush the queue synchronously to get the latest updates by calling tick().

Note You can read more about microtasks on [MDN][mdn-microtasks].

import { signal } from '@maverick-js/signals';

const $a = signal(10);

$a.set(10);
$a.set(20);
$a.set(30); // only this write is applied
import { signal, tick } from '@maverick-js/signals';

const $a = signal(10);

// All writes are applied.
$a.set(10);
tick();
$a.set(20);
tick();
$a.set(30);

computedMap

Note Same implementation as indexArray in Solid JS. Prefer computedKeyedMap when referential checks are required.

Reactive map helper that caches each item by index to reduce unnecessary mapping on updates. It only runs the mapping function once per item and adds/removes as needed. In a non-keyed map like this the index is fixed but value can change (opposite of a keyed map).

import { signal, tick } from '@maverick-js/signals';
import { computedMap } from '@maverick-js/signals/map';

const source = signal([1, 2, 3]);

const map = computedMap(source, (value, index) => {
  return {
    i: index,
    get id() {
      return value() * 2;
    },
  };
});

console.log(map()); // logs `[{ i: 0, id: $2 }, { i: 1, id: $4 }, { i: 2, id: $6 }]`

source.set([3, 2, 1]);
tick();

// Notice the index `i` remains fixed but `id` has updated.
console.log(map()); // logs `[{ i: 0, id: $6 }, { i: 1, id: $4 }, { i: 2, id: $2 }]`

computedKeyedMap

Note Same implementation as mapArray in Solid JS. Prefer computedMap when working with primitives to avoid unnecessary re-renders.

Reactive map helper that caches each list item by reference to reduce unnecessary mapping on updates. It only runs the mapping function once per item and then moves or removes it as needed. In a keyed map like this the value is fixed but the index changes (opposite of non-keyed map).

import { signal, tick } from '@maverick-js/signals';
import { computedKeyedMap } from '@maverick-js/signals/map';

const source = signal([{ id: 0 }, { id: 1 }, { id: 2 }]);

const nodes = computedKeyedMap(source, (value, index) => {
  const div = document.createElement('div');

  div.setAttribute('id', String(value.id));
  Object.defineProperty(div, 'i', {
    get() {
      return index();
    },
  });

  return div;
});

console.log(nodes()); // [{ id: 0, i: $0 }, { id: 1, i: $1 }, { id: 2, i: $2 }];

source.set((prev) => {
  // Swap index 0 and 1

Related Skills

View on GitHub
GitHub Stars887
CategoryDevelopment
Updated23h ago
Forks24

Languages

TypeScript

Security Score

100/100

Audited on Mar 24, 2026

No findings