npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@genexus/kasstor-signals

v0.3.0

Published

Reactive signals for state management; works with any JS app and provides a decorator and directives for Lit components built with @genexus/kasstor-core. Built on alien-signals.

Readme

@genexus/kasstor-signals

A reactive signals system for state management that works with any JavaScript application and provides a decorator and directives for seamless integration with Lit components built with @genexus/kasstor-core. Built on alien-signals.

Table of Contents

Installation

npm i @genexus/kasstor-signals

Quick example (Lit + watch)

In Lit, wrap every signal you render in the template with watch so that part updates when the signal changes:

import { Component, KasstorElement } from "@genexus/kasstor-core/decorators/component.js";
import { computed, signal } from "@genexus/kasstor-signals/core.js";
import { watch } from "@genexus/kasstor-signals/directives/watch.js";
import { html } from "lit";

const count = signal(0);
const doubled = computed(() => count() * 2);

/**
 * Counter that displays a signal and its doubled value; uses watch in template.
 * @access public
 */
@Component({ tag: "my-counter" })
export class MyCounter extends KasstorElement {
  #incrementCount = (): void => {
    count(count() + 1);
  };

  override render() {
    return html`
      <p>Count: ${watch(count)}</p>
      <p>Doubled: ${watch(doubled)}</p>
      <button @click=${this.#incrementCount}>Increment count (+1)</button>
    `;
  }
}

Core Concepts

What are signals?

Signals are data structures for managing observable state. A signal holds a value (or a computed value that depends on other signals). When a signal changes, consumers that depend on it are notified. Because signals form a dependency graph, computed values re-compute and effects re-run when their dependencies change. Signals are well-suited for shared state: values that many components may read or update.

Signal APIs typically have three main concepts:

  • State signals — Hold a single value (e.g. signal(0)). Read and write the value; dependents are notified on write.
  • Computed signals — Wrap a computation that depends on other signals (e.g. computed(() => a() + b())). Memoized; recompute when dependencies change.
  • Watchers / effects — Run side-effectful code when signal values change (e.g. effect(() => { ... })). Used to sync state, update the DOM, or trigger component updates.

Why signals?

  • Automatic dependency tracking: Effects and computed values automatically know which signals they depend on.

  • Minimal updates: Only the code that depends on changed signals runs again (or, in Lit with watch, only the bound parts of the template).

  • Simple API: Create reactive state with a function call; no manual subscription management.

  • Framework agnostic: Works with vanilla JS, Lit, or any framework.

Using signals with Lit

Use watch in templates. Changing a signal does not trigger a Lit component update. To have the template update, you must subscribe where you read the signal: use the watch directive for each place you render a signal (or use an effect + requestUpdate() for a full re-render—see Pro tip below).

This is a design choice for performance: only the parts wrapped in watch re-render (pin-point updates).

Based on alien-signals

This package is based on alien-signals, a minimal, push-pull signal library. Alien-signals is designed for performance (no Array/Set/Map in the core, no recursion in the algorithm), fine-grained reactivity, and a simple API. Its algorithm is related to Vue 3’s propagation, Preact’s double-linked-list approach, and Svelte’s effect scheduling. The core has been adopted by Vue 3.6. We re-export and document the core primitives, add general utilities (e.g. batch, untrack) that work in any JavaScript environment, and Lit-specific utilities (decorators, directives).

API

Core

Core primitives are re-exported from the package and can be used in any JavaScript environment. Import from @genexus/kasstor-signals or @genexus/kasstor-signals/core.js. You can also import individual utilities from subpaths (e.g. @genexus/kasstor-signals/core/signal.js, @genexus/kasstor-signals/core/batch.js). Signals are getter/setter functions: call with no args to read, with one arg to write.

signal

Creates a reactive value. The returned function is the signal: call with no arguments to read, call with one argument to set (setter returns void).

  • Behavior:
    • Reading the signal (e.g. inside computed or effect) tracks it as a dependency.
    • Setting the value notifies dependents. Updates can be batched with batch.

Example

import { signal } from "@genexus/kasstor-signals/core/signal.js";

const count = signal(0);

console.log(count()); // 0
count(5);
console.log(count()); // 5

In a Lit component (with watch)

import {
  Component,
  KasstorElement
} from "@genexus/kasstor-core/decorators/component.js";
import { computed, signal } from "@genexus/kasstor-signals/core.js";
import { watch } from "@genexus/kasstor-signals/directives/watch.js";
import { html } from "lit";

const count = signal(0);
const doubleCount = computed(() => count() * 2);

/**
 * Counter that displays a signal and its doubled value; uses watch in template.
 * @access public
 */
@Component({ tag: "my-counter" })
export class MyCounter extends KasstorElement {
  #incrementCount = () => {
    count(count() + 1);
  };

  override render() {
    return html`
      <p>Count: ${watch(count)}</p>
      <p>Double count: ${watch(doubleCount)}</p>
      <button @click=${this.#incrementCount}>Increment</button>
    `;
  }
}

computed

Creates a memoized derived value. Read-only. The computation runs only when someone reads the value (e.g. calls the getter); if nobody reads it, the computation is not executed. When dependencies change, the computed is marked for recomputation and runs again on the next read.

  • Behavior:
    • Lazy / on read: the getter runs only when the computed is read. If no effect or code reads it, the computation does not run. When dependencies change, the computed runs again on the next read.
    • Only signals (and computeds) read during the run are tracked as dependencies.
    • Use untrack inside the function to read a value without adding a dependency.

Example

import { signal, computed } from "@genexus/kasstor-signals/core.js";

const firstName = signal("John");
const lastName = signal("Doe");

const fullName = computed(() => `${firstName()} ${lastName()}`);

console.log(fullName()); // "John Doe"
firstName("Jane");
console.log(fullName()); // "Jane Doe"

Example (real-world)

import {
  Component,
  KasstorElement
} from "@genexus/kasstor-core/decorators/component.js";
import { signal, computed } from "@genexus/kasstor-signals/core.js";
import { watch } from "@genexus/kasstor-signals/directives/watch.js";
import { html } from "lit";

const items = signal([
  { id: 1, name: "Item 1", price: 100 },
  { id: 2, name: "Item 2", price: 200 }
]);

const totalPrice = computed(() => {
  return items().reduce((sum, item) => sum + item.price, 0);
});

const itemCount = computed(() => items().length);

/**
 * Cart summary that displays item count and total price from shared signals.
 * @access public
 */
@Component({ tag: "app-cart" })
export class AppCart extends KasstorElement {
  override render() {
    return html`
      <div>
        <p>Items: ${watch(itemCount)}</p>
        <p>Total: $${watch(totalPrice)}</p>
      </div>
    `;
  }
}

effect

Runs a side effect that re-runs whenever its dependencies change. Use for syncing to localStorage, updating the DOM, or running logic when specific signals change. In a Lit component, you can use an effect that reads the signal(s) and calls this.requestUpdate() so the component re-renders when the signal changes; start the effect in connectedCallback and call the returned stop function in disconnectedCallback so it is cleaned up when the element is removed and re-created when re-inserted (see Pro tip: forcing a full component update). The effect is not auto-disposed when nothing references it—you must call the stop function to remove the subscription.

  • Behavior:
    • Runs once immediately, then again whenever any tracked signal/computed read inside the function changes.
    • Return value is a function that stops the effect (call it to avoid memory leaks and to clean up when the component is disconnected).

Example

import { signal, effect } from "@genexus/kasstor-signals/core.js";

const theme = signal("light");

// Sync theme to localStorage and DOM whenever it changes
effect(() => {
  const value = theme();
  localStorage.setItem("theme", value);
  document.documentElement.setAttribute("data-theme", value);
});

theme("dark"); // Effect runs again: storage and DOM update

Example (effectScope)

Use effectScope when you need to run effects and dispose them together (e.g. when a component or feature unmounts). Pass a callback that creates effects; the returned function stops the scope and disposes all of them.

import { signal, effect, effectScope } from "@genexus/kasstor-signals/core.js";

const searchQuery = signal("");

const stopScope = effectScope(() => {
  effect(() => {
    console.log("Search changed:", searchQuery());
  });
});

searchQuery("hello"); // Logs: "Search changed: hello"

// Clean up all effects in this scope
stopScope();
searchQuery("world"); // Nothing logs; effect is disposed

effectScope

Groups multiple reactive effects and computeds into a single scope and returns a stop function. Calling it disposes all effects created inside, so you control them as one unit. Useful for modular, reusable logic and to avoid memory leaks in long-lived applications.

  • Behavior: The callback runs immediately; effects/computeds created inside are tied to the scope. The returned function stops the scope and disposes all of them. Nested scopes are supported: stopping a parent stops its children.

  • Use cases: Avoid memory leaks by stopping the scope when a component or feature unmounts; group reactive logic into reusable modules that can be cleanly started or stopped.

trigger

Manually notifies a signal’s dependents without changing its value. Use when you mutate a value in place (e.g. push into an array stored in the signal); the setter is never called, so dependents would not run otherwise.

  • Utility:
    • After in-place mutation, call trigger(signal) so computed values and effects that depend on that signal recompute.
    • To notify multiple signals at once, pass a function that reads them: trigger(() => { src1(); src2(); }).

Example (trigger after mutation)

import { signal, computed, trigger } from "@genexus/kasstor-signals/core.js";

const arr = signal<number[]>([]);
const length = computed(() => arr().length);

console.log(length()); // 0

arr().push(1);
console.log(length()); // Still 0

trigger(arr);
console.log(length()); // 1

Example (trigger multiple signals)

import { signal, computed, trigger } from "@genexus/kasstor-signals/core.js";

const src1 = signal<number[]>([]);
const src2 = signal<number[]>([]);
const total = computed(() => src1().length + src2().length);

src1().push(1);
src2().push(2);

trigger(() => {
  src1();
  src2();
});
console.log(total()); // 2

Decorators

Decorators turn class members into reactive signals or wire them to the signals system.

SignalProp

Turns a class property into a reactive signal. Read and write the property normally; use $propName for the raw signal when passing to watch or when you need the signal function (e.g. trigger(this.$count)).

  • Behavior:

    • The property getter/setter uses an underlying signal. Changes to the property do not trigger component re-renders—in Lit, use the watch directive in the template so the UI updates when the value changes.
    • You can use the property inside computed and effect: reading this.propName tracks the underlying signal, so the computed or effect updates when the property changes. Use watch(this.propName) or watch(this.$propName) in templates.
  • Typing this.$propName: For correct TypeScript types when using this.$propName, add a declare for the raw signal: declare $propName: KasstorSignalState<MyClass["propName"]>;. Import the type from the package: import type { KasstorSignalState } from "@genexus/kasstor-signals".

  • Restrictions: Apply to class instance fields. Initializer value is the signal's initial value.

Example

import {
  Component,
  KasstorElement
} from "@genexus/kasstor-core/decorators/component.js";
import { SignalProp } from "@genexus/kasstor-signals/decorators/signal-prop.js";
import type { KasstorSignalState } from "@genexus/kasstor-signals";
import { watch } from "@genexus/kasstor-signals/directives/watch.js";
import { html } from "lit";

/**
 * Counter with a reactive count via @SignalProp; uses watch in template.
 * @access public
 */
@Component({ tag: "app-counter" })
export class AppCounter extends KasstorElement {
  declare $count: KasstorSignalState<AppCounter["count"]>;

  /** Current counter value. */
  @SignalProp count: number = 0;

  #onIncrement = (): void => {
    this.count++;
  };

  render() {
    return html`
      <p>Count: ${watch(this.$count)}</p>
      <button @click=${this.#onIncrement}>Increment</button>
    `;
  }
}

Example (computed and effect with SignalProp)

You can derive values with computed and run side effects with effect from a @SignalProp property; reading the property tracks it as a dependency.

import { computed, effect } from "@genexus/kasstor-signals/core.js";
import { SignalProp } from "@genexus/kasstor-signals/decorators/signal-prop.js";

class Counter {
  @SignalProp count = 1;
  @SignalProp step = 2;

  total = computed(() => this.count * this.step);
}

const c = new Counter();
console.log(c.total()); // 2

c.count = 10;
console.log(c.total()); // 20

// effect re-runs when a tracked SignalProp changes
const stop = effect(() => {
  console.log("count is", c.count);
});
c.count = 5; // effect runs again
stop();

Example (multiple SignalProps)

import {
  Component,
  KasstorElement
} from "@genexus/kasstor-core/decorators/component.js";
import { SignalProp } from "@genexus/kasstor-signals/decorators/signal-prop.js";
import { watch } from "@genexus/kasstor-signals/directives/watch.js";
import { html } from "lit";
import { property } from "lit/decorators/property.js";
import { state } from "lit/decorators/state.js";

/**
 * User profile that loads name and email by userId; uses SignalProp for reactive props.
 * @access public
 */
@Component({ tag: "app-user-profile" })
export class AppUserProfile extends KasstorElement {
  @state() isLoading = false;

  /** Id of the user to load; when set, profile data is fetched. */
  @property() userId: string = "";

  /** User full name. */
  @SignalProp name: string = "";

  /** User email address. */
  @SignalProp email: string = "";

  protected async updated(
    changedProperties: Map<PropertyKey, unknown>
  ): Promise<void> {
    if (changedProperties.has("userId") && this.userId) {
      await this.#loadUserData();
    }
  }

  #loadUserData = async (): Promise<void> => {
    this.isLoading = true; // Triggers update
    try {
      const response = await fetch(`/api/users/${this.userId}`);
      const data = await response.json();
      this.name = data.name;
      this.email = data.email;
    } finally {
      this.isLoading = false; // Triggers update
    }
  };

  override render() {
    if (this.isLoading) {
      return html`<p>Loading...</p>`;
    }

    return html`
      <div>
        <p><strong>Name:</strong> ${watch(this.name)}</p>
        <p><strong>Email:</strong> ${watch(this.email)}</p>
      </div>
    `;
  }
}

Directives

Directives are used in Lit templates to subscribe to signals and update only the bound part when the value changes.

watch

Essential for Lit: Without watch, Lit templates do not update when a signal changes. Changing a signal does not trigger a component update. Always use watch(signal) in the template wherever you render a signal so that part subscribes and re-renders.

Pin-point updates: Only the bindings wrapped in watch are updated when their signal changes; the rest of the template is skipped. Updates from watch participate in the Lit reactive update lifecycle. The benefit scales with template size: more bindings and logic mean more work skipped when only a few signals change. Wrap each signal read in the template with watch(signal) so that:

  • The current value is rendered.

  • That part of the template subscribes to the signal and re-renders when the value changes.

  • Behavior: Renders the current value and subscribes; if the host has a pending update when the signal changes, the part updates in that cycle, otherwise in a microtask. Works with SSR.

  • Restrictions: Pass a signal or computed (getter function), not a plain value.

Example

import {
  Component,
  KasstorElement
} from "@genexus/kasstor-core/decorators/component.js";
import { signal } from "@genexus/kasstor-signals/core/signal.js";
import { watch } from "@genexus/kasstor-signals/directives/watch.js";
import { html } from "lit";

const notificationCount = signal(0);

/**
 * Notifications header with badge count from a shared signal; uses watch in template.
 * @access public
 */
@Component({ tag: "app-notifications" })
export class AppNotifications extends KasstorElement {
  #onNotify = (): void => {
    notificationCount(notificationCount() + 1);
  };

  render() {
    return html`
      <header>
        <span class="badge">${watch(notificationCount)}</span>
      </header>
      <button @click=${this.#onNotify}>Notify</button>
    `;
  }
}

Example (complex)

import {
  Component,
  KasstorElement
} from "@genexus/kasstor-core/decorators/component.js";
import { signal, computed } from "@genexus/kasstor-signals/core/signal.js";
import { watch } from "@genexus/kasstor-signals/directives/watch.js";
import { html } from "lit";

const todoList = signal([
  { id: 1, text: "Learn Signals", completed: false },
  { id: 2, text: "Build App", completed: true }
]);

const todoListTemplate = computed(() => {
  const todos = todoList();
  return html`
    ${todos.map(
      todo => html`<li ?data-completed=${todo.completed}>${todo.text}</li>`
    )}
  `;
});

/**
 * Todo list that renders items from a shared signal template computed; uses watch.
 * @access public
 */
@Component({ tag: "app-todo-list" })
export class AppTodoList extends KasstorElement {
  render() {
    return html`<ul>
      ${watch(todoListTemplate)}
    </ul>`;
  }
}

batch

Runs the callback and flushes all signal updates once it completes. Improves performance: computed values and effects that track multiple dependencies run only once when you change several of those dependencies inside the batch, instead of once per changed signal.

  • Behavior:
    • All signal writes inside fn are deferred; dependents (computed, effect) run only after fn returns.
    • Nested batches are supported; the outer batch flushes when its callback completes.
    • Reading a signal inside the batch sees the updated value.
    • fn is synchronous; returns the return value of fn.

Example (without batch vs with batch)

import {
  batch,
  signal,
  computed,
  effect
} from "@genexus/kasstor-signals/core.js";

const firstName = signal("John");
const lastName = signal("Doe");
const fullName = computed(() => `${firstName()} ${lastName()}`);

effect(() => {
  console.log("fullName is", fullName());
});
// Logs once: "fullName is John Doe"

// Without batch: effect runs after each write
firstName("Jane"); // Logs: "fullName is Jane Doe"
lastName("Smith"); // Logs again: "fullName is Jane Smith"

// With batch: effect runs once at the end
batch(() => {
  firstName("Alice");
  lastName("Brown");
});
// Logs once: "fullName is Alice Brown"

untrack

Runs the function without tracking any signal reads. Use inside a computed or effect when you need a value without adding a dependency.

  • Behavior:
    • Any signal/computed read inside fn does not register as a dependency of the current effect or computed.
    • Common use: in an effect that reads several signals, wrap the ones you don’t want to track so the effect only re-runs when the others change.

Example

import { signal, effect, untrack } from "@genexus/kasstor-signals/core.js";

const userName = signal("Alice");
const theme = signal("light");
const logLevel = signal("info");

// Re-run only when userName or theme changes; read logLevel without tracking it
effect(() => {
  const name = userName();
  const themeValue = theme();
  const level = untrack(() => logLevel()); // not a dependency

  console.log(`[${level}] User ${name}, theme ${themeValue}`);
});
// Logs: "[info] User Alice, theme light"

userName("Bob"); // Logs again (we track userName)
theme("dark"); // Logs again (we track theme)
logLevel("debug"); // Does not log (we don't track logLevel)

Best Practices

Signal Organization

Create a signals store for shared state:

// signals/app-store.ts
import { signal, computed, effect } from "@genexus/kasstor-signals/core.js";

export interface User {
  id: string;
  name: string;
  email: string;
}

// State signals
export const user = signal<User | null>(null);
export const isLoading = signal(false);
export const error = signal<string | null>(null);

// Computed signals
export const isAuthenticated = computed(() => user() !== null);
export const displayName = computed(() => user()?.name ?? "Guest");

// Side effects
effect(() => {
  if (user()) {
    localStorage.setItem("user", JSON.stringify(user()));
  }
});

Using the Store in Components

When a component reads signals from a store in its template, the component must subscribe so it re-renders when those signals change. Either use watch for each signal in the template (pin-point updates) or run an effect that reads the signals and calls this.requestUpdate() so the whole component updates. Use connectedCallback to start the effect and disconnectedCallback to stop it (so the effect is cleaned up when the element is removed and re-created when it is re-inserted, e.g. when moved in the DOM). The effect is not auto-disposed by the library—you must call the returned stop function.

import { Component, KasstorElement } from "@genexus/kasstor-core/decorators/component.js";
import { effect } from "@genexus/kasstor-signals/core/effect.js";
import { displayName, isLoading } from "../signals/app-store";
import { html } from "lit";

/**
 * Header that subscribes to store signals via effect and requestUpdate.
 * @access public
 */
@Component({ tag: "app-header" })
export class AppHeader extends KasstorElement {
  #stopEffect?: () => void;

  override connectedCallback(): void {
    super.connectedCallback();
    this.#stopEffect = effect(() => {
      isLoading();
      displayName();
      this.requestUpdate();
    });
  }

  override disconnectedCallback(): void {
    this.#stopEffect?.();
    super.disconnectedCallback();
  }

  override render() {
    return html`
      <header>
        ${isLoading()
          ? html`<p>Loading...</p>`
          : html`<h1>Welcome ${displayName()}</h1>`}
      </header>
    `;
  }
}

Avoid Over-Tracking

Use untrack when you read a signal for display (or side effect) but don’t want changes to that signal to trigger a re-run. The computed or effect should have at least one tracked dependency so it can update when needed; use untrack only on the reads you want to exclude.

const itemCount = signal(0);
const logLevel = signal("info");

// ✗ Bad: Re-runs when either changes; if we only care about itemCount, we over-track
const display = computed(() => {
  const count = itemCount();
  const level = logLevel();
  return `[${level}] Count: ${count}`;
});

// ✓ Good: Re-runs only when itemCount changes; logLevel is read but not a dependency
const display = computed(() => {
  const count = itemCount();
  const level = untrack(() => logLevel());
  return `[${level}] Count: ${count}`;
});

Do's and Don'ts

Do

  • Create a signals store (or module) for shared state; keep signals and computed values in one place.

  • Use batch() when updating multiple signals in one logical step so dependents run once.

  • Use watch in Lit templates to subscribe only the template part that needs the signal.

  • Use a method or stored function reference for event handlers (e.g. @click=${this.#handler}) instead of inline arrow functions, so the reference is not re-created on every render.

  • Use @SignalProp for component props that should be reactive signals.

  • Use untrack() when you need a signal's value without adding it as a dependency.

Don't

  • Create unnecessary dependencies in computed/effect (causes extra recomputation; use untrack when you only need a value).

  • Read a signal in a Lit template without watch (the template will not update when the signal changes).

  • Mix signal and non-signal state in the same component without a clear pattern.

Pro tip: when to use batch vs untrack

Use batch when you are updating several signals in one logical step (e.g. after a fetch or in one event handler) so dependents run once. Use untrack when a computed or effect needs to read a signal's value without subscribing to it (e.g. for comparison or logging without creating a dependency).

Pro tip: forcing a full component update when a signal changes

  • Preferred: Use watch in the template so only the bound parts re-render.
  • When you need the whole component to re-render: Use an effect that reads the signal(s) and calls this.requestUpdate().
    • Start the effect in connectedCallback, call the returned stop function in disconnectedCallback (so it cleans up when the element is removed and re-creates when re-inserted).
    • The effect is not auto-disposed—you must call the stop function.
import {
  Component,
  KasstorElement
} from "@genexus/kasstor-core/decorators/component.js";
import { effect } from "@genexus/kasstor-signals/core/effect.js";
import { signal } from "@genexus/kasstor-signals/core/signal.js";
import { html } from "lit";

const searchQuery = signal("");

/**
 * Search UI that subscribes to searchQuery via effect and requestUpdate.
 * @access public
 */
@Component({ tag: "app-search" })
export class AppSearch extends KasstorElement {
  #stopEffect?: () => void;

  override connectedCallback(): void {
    super.connectedCallback();
    this.#stopEffect = effect(() => {
      searchQuery(); // subscribe to signal
      this.requestUpdate();
    });
  }

  override disconnectedCallback(): void {
    this.#stopEffect?.();
    super.disconnectedCallback();
  }

  override render() {
    return html`<p>Query: ${searchQuery()}</p>`;
  }
}

API Reference

Core

  • signal<T>(initialValue: T) — Returns a getter/setter function: call with no args to read, one arg to set (setter returns void).

  • computed<T>(fn: () => T) — Returns a memoized getter.

    • Computation runs only when the value is read; if nobody reads it, it does not run.
    • Only signals/computeds read during the run are tracked; recomputes on next read when dependencies change.
  • effect(fn: () => void) — Runs the function and re-runs when dependencies change.

    • Returns a stop function (call it to remove the subscription; the effect is not auto-disposed).
  • batch<T>(fn: () => T) — Runs fn (synchronous); defers signal updates and flushes when fn completes.

    • Improves performance: computeds and effects that track multiple dependencies run only once when you change several of them in the batch.
    • Returns the return value of fn. Nested batches supported; reading a signal inside sees the updated value.
  • untrack<T>(fn: () => T) — Runs fn without tracking signal reads; returns the return value of fn. Use inside computed/effect to read a value without adding a dependency.

  • effectScope(fn) — Runs the callback (which can create effects/computeds) and returns a stop function.

    • Call it to dispose all effects in the scope. Nested scopes: stopping a parent stops its children.
    • Use to avoid memory leaks and for scoped state management.
  • trigger(target) — Manually notifies a signal’s dependents without changing its value.

    • Use after in-place mutation (e.g. arr().push(1) then trigger(arr)).
    • To trigger multiple signals: trigger(() => { src1(); src2(); }).
  • Type guards: isSignal, isComputed, isEffect, isEffectScope — Return true if the value is the corresponding reactive primitive.

Decorators

  • @SignalProp — Class decorator: turns a property into a reactive signal.
    • Read/write the property normally; use $propName for the raw signal (e.g. for watch or trigger).
    • For TypeScript typing of this.$propName, add declare $propName: KasstorSignalState<MyClass["propName"]>; (import KasstorSignalState from the package).
    • Changes to the property do not trigger component re-renders—use watch in the template.
    • You can use the property inside computed and effect (reading it tracks the signal).
    • Apply to class instance fields; initializer is the signal’s initial value. Import from @genexus/kasstor-signals/decorators/signal-prop.js.

Directives

  • watch(signal) — Lit directive: subscribes to a signal (or computed) in a template and re-renders only that part when the value changes.
    • Without watch, Lit templates do not update when a signal changes.
    • Pass a signal/computed getter; returns the current value for rendering. Import from @genexus/kasstor-signals/directives/watch.js.

Contributing

Kasstor is open source and we appreciate issue reports and pull requests. See CONTRIBUTING.md for more information.