Skip to content

Watch

AkashJS provides watch() and related utilities for observing reactive values with fine-grained control over timing, frequency, and structure. While effect() is great for simple side-effects, watch() gives you access to old and new values, one-time execution, debouncing, and more.

watch() vs effect()

Use effect() when you want to run code whenever any tracked dependency changes and you don't need the previous value. Use watch() when you need:

  • Access to old and new values
  • To watch a specific source (not auto-tracked)
  • One-time or debounced execution
  • Explicit control over when watching starts
ts
import { signal, effect } from '@akashjs/runtime';
import { watch } from '@akashjs/runtime';

const count = signal(0);

// effect — auto-tracks, no old value
effect(() => {
  console.log(`Count is ${count()}`);
});

// watch — explicit source, old + new
watch(count, (newVal, oldVal) => {
  console.log(`Count changed from ${oldVal} to ${newVal}`);
});

Basic watch

watch() takes a source signal (or getter function) and a callback that receives the new and old values. It returns a dispose function.

ts
import { signal } from '@akashjs/runtime';
import { watch } from '@akashjs/runtime';

const name = signal('Alice');

const stop = watch(name, (newName, oldName) => {
  console.log(`Name changed: ${oldName} -> ${newName}`);
});

name.set('Bob');
// Logs: "Name changed: Alice -> Bob"

stop(); // stop watching

You can also pass a getter function as the source:

ts
const first = signal('Alice');
const last = signal('Smith');

watch(
  () => `${first()} ${last()}`,
  (newFull, oldFull) => {
    console.log(`Full name: ${oldFull} -> ${newFull}`);
  },
);

Immediate option

By default, watch() only fires on changes after setup. Pass immediate: true to run the callback immediately with the current value.

ts
const theme = signal('light');

watch(theme, (newTheme) => {
  document.body.className = newTheme;
}, { immediate: true });
// Runs immediately with "light", then on every change

Once option

Pass once: true to automatically dispose the watcher after the first change.

ts
const status = signal('pending');

watch(status, (newStatus) => {
  console.log(`Status resolved to: ${newStatus}`);
}, { once: true });

status.set('complete');
// Logs once, then watcher is disposed
status.set('archived');
// No log — watcher already stopped

watchOnce

A convenience shorthand for watch(source, cb, { once: true }).

ts
import { watchOnce } from '@akashjs/runtime';

const ready = signal(false);

watchOnce(ready, (isReady) => {
  if (isReady) bootstrap();
});

watchDebounced

watchDebounced() delays the callback until the source stops changing for a given number of milliseconds. Ideal for search inputs.

ts
import { signal } from '@akashjs/runtime';
import { watchDebounced } from '@akashjs/runtime';

const query = signal('');

watchDebounced(query, async (search) => {
  const results = await api.search(search);
  hits.set(results);
}, { wait: 300 });

Options:

OptionTypeDefaultDescription
waitnumber250Debounce delay in ms
immediatebooleanfalseRun callback immediately on setup
maxWaitnumberundefinedMaximum time to wait before forcing a call

Watching multiple sources

Pass an array of sources to watch several signals at once. The callback receives arrays of new and old values.

ts
import { signal } from '@akashjs/runtime';
import { watch } from '@akashjs/runtime';

const lat = signal(0);
const lng = signal(0);

watch([lat, lng], ([newLat, newLng], [oldLat, oldLng]) => {
  console.log(`Moved from (${oldLat}, ${oldLng}) to (${newLat}, ${newLng})`);
});

Deep watching

By default, watch() compares values by reference. For objects and arrays, pass deep: true to detect nested changes.

ts
const filters = signal({ status: 'active', page: 1 });

watch(filters, (newFilters, oldFilters) => {
  console.log('Filters changed', newFilters);
}, { deep: true });

filters.update(f => ({ ...f, page: 2 }));
// Fires because deep comparison detects the change

deepSignal()

For deeply nested reactive objects, deepSignal() wraps an entire object tree so every property — at any depth — is reactive.

ts
import { deepSignal } from '@akashjs/runtime';

const state = deepSignal({
  user: {
    name: 'Alice',
    prefs: { theme: 'dark', lang: 'en' },
  },
  items: [1, 2, 3],
});

// Read nested properties reactively
effect(() => {
  console.log(state.user.prefs.theme);
});

// Write nested properties directly
state.user.prefs.theme = 'light';
// Effect re-runs: "light"

$raw and toRaw

Access the underlying plain object without creating reactive proxies:

ts
import { deepSignal, toRaw } from '@akashjs/runtime';

const state = deepSignal({ count: 0, nested: { x: 1 } });

// $raw gives the unwrapped snapshot at that level
console.log(state.$raw); // { count: 0, nested: { x: 1 } }

// toRaw() fully unwraps a deep signal at any depth
const plain = toRaw(state.nested);
console.log(plain); // { x: 1 } — plain object, not reactive

isDeepSignal

Check whether a value is a deep signal proxy:

ts
import { deepSignal, isDeepSignal } from '@akashjs/runtime';

const state = deepSignal({ x: 1 });

isDeepSignal(state);   // true
isDeepSignal({ x: 1 }); // false

Event Bus

createEventBus() provides a lightweight, typed publish/subscribe system for decoupled communication between parts of your application.

ts
import { createEventBus } from '@akashjs/runtime';

interface Events {
  'user:login': { id: string; name: string };
  'user:logout': void;
  'cart:update': { itemCount: number };
}

const bus = createEventBus<Events>();

on / emit

Subscribe to events with on(), which returns an unsubscribe function. Emit events with emit().

ts
const unsub = bus.on('user:login', (user) => {
  console.log(`Welcome, ${user.name}`);
});

bus.emit('user:login', { id: '1', name: 'Alice' });
// Logs: "Welcome, Alice"

unsub(); // stop listening

once

Listen for a single occurrence of an event, then automatically unsubscribe.

ts
bus.once('user:login', (user) => {
  analytics.track('first_login', user);
});

off

Remove a specific handler:

ts
function handler(user: Events['user:login']) {
  console.log(user.name);
}

bus.on('user:login', handler);
bus.off('user:login', handler);

clear

Remove all listeners for a specific event, or all listeners entirely:

ts
bus.clear('user:login'); // clear listeners for one event
bus.clear();             // clear all listeners

globalEventBus

A pre-created, untyped event bus available anywhere in your app for quick prototyping or simple use cases.

ts
import { globalEventBus } from '@akashjs/runtime';

globalEventBus.on('notify', (msg) => {
  showToast(msg);
});

// Somewhere else
globalEventBus.emit('notify', 'Item saved!');

For production apps, prefer creating typed event buses with createEventBus<Events>() to get full type safety.

Released under the MIT License.