@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
- Quick example (Lit + watch)
- Core Concepts
- Based on alien-signals
- API
- Best Practices
- API Reference
- Contributing
Installation
npm i @genexus/kasstor-signalsQuick 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
watchin 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 thewatchdirective 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
computedoreffect) tracks it as a dependency. - Setting the value notifies dependents. Updates can be batched with
batch.
- Reading the signal (e.g. inside
Example
import { signal } from "@genexus/kasstor-signals/core/signal.js";
const count = signal(0);
console.log(count()); // 0
count(5);
console.log(count()); // 5In 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
untrackinside 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 updateExample (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 disposedeffectScope
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(); }).
- After in-place mutation, call
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()); // 1Example (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()); // 2Decorators
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
watchdirective in the template so the UI updates when the value changes. - You can use the property inside
computedandeffect: readingthis.propNametracks the underlying signal, so the computed or effect updates when the property changes. Usewatch(this.propName)orwatch(this.$propName)in templates.
- The property getter/setter uses an underlying signal. Changes to the property do not trigger component re-renders—in Lit, use the
Typing
this.$propName: For correct TypeScript types when usingthis.$propName, add adeclarefor 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 usewatch(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
fnare deferred; dependents (computed, effect) run only afterfnreturns. - Nested batches are supported; the outer batch flushes when its callback completes.
- Reading a signal inside the batch sees the updated value.
fnis synchronous; returns the return value offn.
- All signal writes inside
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
fndoes 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.
- Any signal/computed read inside
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
watchin 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
@SignalPropfor 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
untrackwhen 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
watchin the template so only the bound parts re-render. - When you need the whole component to re-render: Use an
effectthat reads the signal(s) and callsthis.requestUpdate().- Start the effect in
connectedCallback, call the returned stop function indisconnectedCallback(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.
- Start the effect in
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)— Runsfn(synchronous); defers signal updates and flushes whenfncompletes.- 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)— Runsfnwithout tracking signal reads; returns the return value offn. 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)thentrigger(arr)). - To trigger multiple signals:
trigger(() => { src1(); src2(); }).
- Use after in-place mutation (e.g.
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
$propNamefor the raw signal (e.g. forwatchortrigger). - For TypeScript typing of
this.$propName, adddeclare $propName: KasstorSignalState<MyClass["propName"]>;(importKasstorSignalStatefrom the package). - Changes to the property do not trigger component re-renders—use
watchin the template. - You can use the property inside
computedandeffect(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.
- Read/write the property normally; use
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.
- Without
Contributing
Kasstor is open source and we appreciate issue reports and pull requests. See CONTRIBUTING.md for more information.
