Skip to content

when · whenTrue · whenFalse

Run a side effect callback when a signal satisfies a condition.

  • Only the source signal drives re-execution — the callback is wrapped in untracked automatically
  • Returns a cancel function that can be stored as a class field and called from a method

The problem they replace:

typescript
// Easy to get wrong — untracked is easy to forget, effects grow noisy over time
effect(() => {
  if (this.isOpen()) {
    untracked(() => {
      this.dashboardCopy.set(cloneDeep(this.dashboard()));
    });
  }
});

// Clear trigger, untracked handled for you
whenTrue(this.isOpen, () => {
  this.dashboardCopy.set(cloneDeep(this.dashboard()));
});

Usage

typescript
import { whenTrue, whenFalse } from 'ng-reactive-utils';

@Component({
  template: `<component-lib-sidebar [isOpen]="isOpen()">...</component-lib-sidebar>`,
})
class EditSidebarComponent {
  isOpen = input(false);
  dashboard = input<Dashboard | null>(null);
  dashboardCopy = signal<Dashboard | null>(null);

  onOpen = whenTrue(this.isOpen, () => {
    this.dashboardCopy.set(cloneDeep(this.dashboard()));
    this.fetchMetadata();
  });

  onClose = whenFalse(this.isOpen, () => {
    this.dashboardCopy.set(null);
  });
}

Custom Condition with when

Use when with a predicate for any condition beyond truthy/falsy.

typescript
import { when } from 'ng-reactive-utils';

type UploadStatus = 'idle' | 'uploading' | 'complete' | 'error';

@Component({ template: `...` })
class FileUploadComponent {
  uploadStatus = signal<UploadStatus>('idle');

  onUploadComplete = when(
    this.uploadStatus,
    (status) => status === 'complete',
    () => {
      this.showSuccessToast();
      this.refreshFileList();
    },
  );

  onUploadError = when(
    this.uploadStatus,
    (status) => status === 'error',
    () => {
      this.showErrorDialog();
    },
  );
}

Early Cancellation

typescript
import { whenTrue } from 'ng-reactive-utils';

@Component({ template: `...` })
class OnboardingComponent {
  isGuideVisible = signal(false);
  stepCount = signal(0);

  onGuideVisible = whenTrue(this.isGuideVisible, () => {
    this.stepCount.update((n) => n + 1);
  });

  permanentlyDismiss() {
    this.isGuideVisible.set(false);
    this.onGuideVisible(); // cancel — no longer reacts to future changes
  }
}

Parameters

when

ParameterTypeDescription
sourceSignal<T>The signal to watch
predicate(value: T) => booleanCondition that determines when callback fires
callback() => voidRuns each time the predicate returns true

whenTrue · whenFalse

ParameterTypeDescription
sourceSignal<unknown>The signal to watch
callback() => voidRuns each time the signal becomes truthy/falsy

Returns

() => void — A cancel function that stops the effect immediately. Safe to call multiple times. Automatic cleanup on component destroy still applies.

Notes

  • Automatic untracked: Signals read inside the callback do not create reactive dependencies. If you need reactive behavior inside the callback, use a separate effect().
  • Runs eagerly: If the condition is already met at creation time, the callback fires on the first effect execution.
  • Runs every time: Fires each time the condition becomes true, not just the first time. For run-once behavior, call the cancel function inside the callback.
  • Truthy semantics: whenTrue uses Boolean(value)'', 0, null, and undefined are treated as falsy.
  • Injection context required: Must be called as a class field initializer, in a constructor, or within runInInjectionContext.

Source

ts
import { DestroyRef, Signal, effect, inject, untracked } from '@angular/core';

/**
 * Runs a callback whenever a signal satisfies a predicate condition. The callback is automatically
 * wrapped in `untracked` so that any signals read or written inside it do not create additional
 * reactive dependencies — only the source signal drives re-execution.
 *
 * @param source - The signal to watch
 * @param predicate - A function that receives the signal's current value and returns true when the callback should run
 * @param callback - The side effect to run when the predicate is satisfied
 * @returns A cancel function that stops the effect immediately. Safe to call multiple times.
 *
 * @example
 * onUploadComplete = when(this.uploadStatus, (status) => status === 'complete', () => {
 *   this.showSuccessToast();
 * });
 */
export function when<T>(
  source: Signal<T>,
  predicate: (value: T) => boolean,
  callback: () => void,
): () => void {
  const destroyRef = inject(DestroyRef);

  const effectRef = effect(() => {
    if (predicate(source())) {
      untracked(callback);
    }
  });

  let isCancelled = false;

  const cancel = () => {
    if (isCancelled) return;
    isCancelled = true;
    effectRef.destroy();
  };

  destroyRef.onDestroy(cancel);

  return cancel;
}

/**
 * Runs a callback each time a signal becomes truthy. The callback is automatically wrapped in
 * `untracked` so that any signals read or written inside it do not create additional reactive
 * dependencies — only the source signal drives re-execution.
 *
 * @param source - The signal to watch
 * @param callback - The side effect to run when the signal becomes truthy
 * @returns A cancel function that stops the effect immediately. Safe to call multiple times.
 *
 * @example
 * onOpen = whenTrue(this.isOpen, () => {
 *   this.dashboardCopy.set(cloneDeep(this.dashboard()));
 * });
 */
export function whenTrue(source: Signal<unknown>, callback: () => void): () => void {
  return when(source, Boolean, callback);
}

/**
 * Runs a callback each time a signal becomes falsy. The callback is automatically wrapped in
 * `untracked` so that any signals read or written inside it do not create additional reactive
 * dependencies — only the source signal drives re-execution.
 *
 * @param source - The signal to watch
 * @param callback - The side effect to run when the signal becomes falsy
 * @returns A cancel function that stops the effect immediately. Safe to call multiple times.
 *
 * @example
 * onClose = whenFalse(this.isOpen, () => {
 *   this.dashboardCopy.set(null);
 * });
 */
export function whenFalse(source: Signal<unknown>, callback: () => void): () => void {
  return when(source, (value) => !value, callback);
}

Released under the MIT License.