untrack()

Read signals without creating reactive subscriptions.

const value = untrack(fn)

Parameters

ParameterTypeDescription
fn () => T A function in which signal reads do not create subscriptions. The function is called synchronously.

Returns

The return value of fn.

ReturnTypeDescription
value T Whatever fn returns. Signals read inside fn return their current values, but the surrounding effect or computed does not subscribe to them.

Usage

Reading Without Subscribing

import { signal, effect, untrack } from 'what-framework';

const count = signal(0);
const multiplier = signal(2);

effect(() => {
  // Re-runs when count changes
  // Does NOT re-run when multiplier changes
  const result = count() * untrack(() => multiplier());
  console.log(result);
});

Preventing Infinite Loops

const items = signal([]);
const log = signal([]);

effect(() => {
  const currentItems = items();
  // Read log without subscribing to avoid cycle
  const currentLog = untrack(() => log());
  log.set([...currentLog, `Items changed: ${currentItems.length}`]);
});

Event Handlers

A handler fired by the browser runs with no tracking context, so signal reads inside it create no subscription. That holds on both render paths, but for different reasons, and the two diverge if you invoke a handler yourself from inside an effect or a computed. Handlers attached through h() are wrapped in untrack() by the DOM layer; compiled JSX routes handlers through event delegation, which does not wrap them, so a handler called from inside a tracking context does subscribe there.

function Counter() {
  const count = signal(0, 'count');
  const step = signal(1, 'step');

  return (
    <button onClick={() => {
      // peek() reads step without subscribing, so calling this handler
      // from inside an effect or computed adds no dependency on step.
      count.set(c => c + step.peek());
    }}>
      {() => `Count: ${count()}`}
    </button>
  );
}

Comparison With peek()

const count = signal(0);

effect(() => {
  // These are equivalent:
  const a = untrack(() => count());
  const b = count.peek();
  // Both read the value without subscribing
});

How It Works

untrack() temporarily sets the current effect tracking context to null while executing fn. Signal reads check this context to decide whether to add a subscriber. With the context cleared, no subscriptions are created. After fn completes, the original context is restored.

Notes

  • untrack() is functionally equivalent to signal.peek() for a single signal, but works with any number of signal reads inside the callback.
  • Use peek() when reading a single known signal. Use untrack() when calling a function that may read multiple signals internally.
  • Handlers attached through h() are wrapped in untrack() by What's DOM layer. Compiled JSX uses event delegation and does not wrap them, so use peek() or untrack() rather than relying on the wrapper.
  • The callback is executed synchronously. It does not defer or schedule anything.
  • Useful for breaking read-write cycles where an effect needs to read a signal that it also writes to.