astro-dx
@astro-dx/core

Signals

signal(), computed(), effect() — same API as Angular 20+

Counter — signal + computed
0 count
+
0 double
neutral sign
Reset

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.

Use A Use B A +1 B +10 Peek value +1
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.
With astro-dx Without
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));