Signals
signal(), computed(), effect() — same API as Angular 20+
Counter — signal + computed
0 count
0 double
neutral sign
Code example
typescript
import { signal, computed } from "@astro-dx/core";
import { getElement } from "@astro-dx/dom";
import { onClick } from "@astro-dx/events";
const count = signal(0);
const double = computed(() => count() * 2);
getElement("#count-display").text(count);
getElement("#double-display").text(double);
onClick("#btn-inc", () => count.update((v) => v + 1));
onClick("#btn-dec", () => count.update((v) => v - 1)); Dynamic deps — conditional effect + peek()
Change branch between A/B and verify re-tracking. Updating peek-only value must not re-run the effect.
Branch
A
Selected value
0
Effect runs
0
A source
1
B source
100
Peek-only signal value: 0
Conditional dependencies snippet
typescript
import { signal, effect } from "@astro-dx/core";
const useA = signal(true);
const a = signal(1);
const b = signal(100);
// Re-tracks dependencies on every run.
effect(() => {
if (useA()) {
console.log("branch A", a());
} else {
console.log("branch B", b());
}
});
// Switch branch -> now b is tracked, a is not.
useA.set(false);
b.set(120); // effect runs
a.set(2); // effect does not run peek() best practices
typescript
import { signal, effect } from "@astro-dx/core";
const visible = signal(true);
const total = signal(99);
const title = signal("Cart");
effect(() => {
if (!visible()) return;
// tracked dependency: this re-runs the effect when title changes
const trackedTitle = title();
// untracked read: changing total does NOT re-run this effect
const currentTotal = total.peek();
console.log(`${trackedTitle}: ${currentTotal}`);
});
// Best practices:
// 1) Prefer normal reads first; use peek() only for truly non-reactive reads.
// 2) Use peek() for analytics/logging/debug values you do not want to subscribe to.
// 3) Keep side effects explicit and return cleanup when wiring external APIs. typescript
import { signal, computed } from "@astro-dx/core";
import { getElement } from "@astro-dx/dom";
import { onClick } from "@astro-dx/events";
const count = signal(0);
const double = computed(() => count() * 2);
const sign = computed(() =>
count() > 0 ? "positive" : count() < 0 ? "negative" : "neutral",
);
getElement("#count-display").text(count);
getElement("#double-display").text(double);
getElement("#sign-display").text(sign);
onClick("#btn-inc", () => count.update((v) => v + 1));
onClick("#btn-dec", () => count.update((v) => v - 1));
onClick("#btn-reset", () => count.set(0)); typescript
import { atom, computed } from "nanostores";
const $count = atom(0);
const $double = computed($count, (c) => c * 2);
const $sign = computed($count, (c) =>
c > 0 ? "positive" : c < 0 ? "negative" : "neutral",
);
$count.subscribe((v) => {
document.querySelector("#count-display")!.textContent = String(v);
});
$double.subscribe((v) => {
document.querySelector("#double-display")!.textContent = String(v);
});
$sign.subscribe((v) => {
document.querySelector("#sign-display")!.textContent = v;
});
document
.querySelector("#btn-inc")
?.addEventListener("click", () => $count.set($count.get() + 1));
document
.querySelector("#btn-dec")
?.addEventListener("click", () => $count.set($count.get() - 1));
document
.querySelector("#btn-reset")
?.addEventListener("click", () => $count.set(0));