Hacking Angular Signals
Tags: angular, webdev, programming
Published: 2024-02-20 09:45:01

Let’s check out what Angular Signals have under the hood, and how we can use it for extending their functionality and debugging.

### What are Angular Signals

Angular Signals are functions with methods:
```ts
    const $val = signal(0);
    
    // function:
    const value = $val();
    
    // object:
    $val.set(4);
    $val.update((v) => v * 2);
```
How is this possible? Because in JavaScript, functions are objects:
```ts
    // declaring a function
    const double = (val: number) => val * 2;
    
    // adding a method
    double.isEven = (number: number) => number % 2 === 0;
    
    // use as object
    double.isEven(15) // false
    
    // use as function
    double(8) // 16
```
How can we use it?

We can:

* add new methods;
* override methods set() and update() to intercept modifications;
* read internal state variables and methods of signal;
* create a proxy signal that will intercept reads.

### Extending our Signals

Sometimes you need a variable to be a Signal, but some parts of your code would like to use it as an Observable instead.

Fortunately, interfaces of Signals and Observables have no collisions in method names. So let’s create a hybrid:
```ts
    export function toObservableSignal<T>(
      s: Signal<T>, 
      options?: ToObservableOptions
    ) {
      if (isDevMode() && !options?.injector) {
        assertInInjectionContext(toObservableSignal);
      }
    
      // create an observable
      const obs = toObservable(s, options);
      
      // add methods of observable to our signal object
      for (const obsKey in obs) {
        (s as any)[obsKey] = (obs as any)[obsKey];
      }
      return s;
    }
```
Usage [example](https://stackblitz.com/edit/stackblitz-starters-5xxadr?file=src%2Fmain.ts):
```ts
    @Component({
      //...
      template: `
        <h4>Signal A: {{ a() }}</h4>
        <h4>Observable A: {{a|async}}</h4>
        <h4>Signal C (computed() A*3): {{c()}}</h4>
    
        {{quote()}}
      `,
    })
    export class App {
      a = toObservableSignal(signal<number>(1));
    
      // use as Observable
      b = this.a.pipe(
        debounceTime(500),
        distinctUntilChanged(),
        switchMap((v) => this.http.get('https://dummyjson.com/quotes/' + v))
      );
    
      // use as Signal
      c = computed(() => this.a() * 3);
    
      quote = toSignal(this.b);
    
      increment() {
        // "a" will not stop being a Signal after 
        // we used it as an Observable
        this.a.update((v) => v + 1);
      }
    
      decrement() {
        this.a.update((v) => Math.max(1, v - 1));
      }
    }
```

You can find this function in the [NG Extension Platform](https://github.com/nartc/ngxtension-platform): [documentation](https://ngxtension.netlify.app/utilities/signals/to-observable-signal/).

This function can be improved in multiple ways, so your pull requests are welcomed!

You can extend Signals with any functions you want, your fantasy here has almost no limitations: just don’t use names `set`, `update`, and `asReadonly`.

### Overriding existing methods

Let’s say that we want to intercept writes to our Signal to transform input values, duplicate information somewhere else, or just for debugging.
```ts
    function skipNonEvenNumbers(s: WritableSignal<number>) {
      const srcSet = s.set; // we need the source method to avoid recursion
    
      s.set = (value: number) => {
        if (value % 2 !== 0) {
          console.warn('[set] skipping:', value);
          return;
        }
        console.log('[set]:', value);
        srcSet(value);
      };
      s.update = (updateFn: (value: number) => number) => {
        const value = updateFn(s());
        if (value % 2 !== 0) {
          console.warn('[update] skipping:', value);
          return;
        }
        console.log('[update]:', value);
        srcSet(value);
      };
    }
```
Usage [example](https://stackblitz.com/edit/stackblitz-starters-yyd2rc?devToolsHeight=50&file=src%2Fmain.ts).

This trick is being used in [Reactive Storage](https://github.com/e-oz/ngx-reactive-storage/): [getWritableSignal()](https://github.com/e-oz/ngx-reactive-storage/blob/main/projects/ngx-reactive-storage/src/lib/observer.ts#L92).

### Inside a Signal

Angular Signal is not just a function, it is an object. This object has a hidden field, `SIGNAL`, which contains some interesting data and functions. I’m glad we have it, and I hope you will not abuse it. Because the tricks above were “a little bit hacky”, and the tricks below are too filthy to use for anything but debugging, creating dev tools, and fun.

Every Angular Signal extends ReactiveNode:
```ts
    // packages/core/primitives/signals/src/graph.ts
    
    export const REACTIVE_NODE: ReactiveNode = {
      version: 0 as Version,
      lastCleanEpoch: 0 as Version,
      dirty: false,
      producerNode: undefined,
      producerLastReadVersion: undefined,
      producerIndexOfThis: undefined,
      nextProducerIndex: 0,
      liveConsumerNode: undefined,
      liveConsumerIndexOfThis: undefined,
      consumerAllowSignalWrites: false,
      consumerIsAlwaysLive: false,
      producerMustRecompute: () => false,
      producerRecomputeValue: () => {},
      consumerMarkedDirty: () => {},
      consumerOnSignalRead: () => {},
    };
```
this way:
```ts
    // packages/core/primitives/signals/src/signal.ts
    
    export const SIGNAL_NODE = {
      ...REACTIVE_NODE,
      equal: defaultEquals,
      value: undefined,
    }
```
But an instantiated Signal object does not contain all of this directly. All of them are hidden under the field that uses a Symbol as a name:
```ts
    // packages/core/primitives/signals/src/signal.ts
    
    export function createSignal<T>(initialValue: T): SignalGetter<T> {
      const node: SignalNode<T> = Object.create(SIGNAL_NODE);
      node.value = initialValue;
      const getter = (() => {
                       producerAccessed(node);
                       return node.value;
                     }) as SignalGetter<T>;
    
      // next line adds a SignalNode to the field SIGNAL:
      (getter as any)[SIGNAL] = node;
      return getter;
    }
```
So if you have a signal $value and you access `SIGNAL` field, then you’ll get all of the fields that `SIGNAL_NODE` has.

How can we use it?

We can [read fields](https://stackblitz.com/edit/stackblitz-starters-8xzgwj?file=src%2Fmain.ts) and override methods to intercept access and use it for debugging or to create fancy tools that illustrate what is going on inside a Signal and render the [Dependency Graph](https://medium.com/@eugeniyoz/dependency-graph-in-angular-signals-53ee47f75e21).

We can even convert some fields to accessors:
```ts
    function getSignalVersion<T>(s: WritableSignal<T>): Signal<number> {
      const node = s[SIGNAL];
      const $version = signal(0);
    
      Object.defineProperty(node, 'version', {
        get: () => {
          const v = untracked($version);
          console.log('🟢 reading:', v);
          return v;
        },
        set: (v) => {
          untracked(() => $version.set(v));
          console.log('🔴 writing:', v);
        },
      });
    
      return $version.asReadonly();
    }
```
[StackBlitz](https://stackblitz.com/edit/stackblitz-starters-scbktp?file=src%2Fmain.ts).

Or create a proxy that can watch Signal reads without `effect()`:
```ts
    function watchSignalReads<T, M extends Signal<T> | WritableSignal<T>>(s: M): M {
      const node = s[SIGNAL];
      const newGetter = () => {
        const value = s();
        console.log('Read:', value);
        return value;
      };
      (newGetter as any)[SIGNAL] = node;
      if (s.hasOwnProperty('set')) {
        const w = s as WritableSignal<T>;
        newGetter.set = w.set;
        newGetter.update = w.update;
        newGetter.asReadonly = w.asReadonly;
      }
    
      return newGetter as M;
    }
```
Usage [example](https://stackblitz.com/edit/stackblitz-starters-jn29gy?devToolsHeight=70&file=src%2Fmain.ts).

Again, I hope you won’t even consider using this in “production,” and instead, I hope you’ll utilize it to create some amazing tools, gain recognition, and contribute to enriching the Angular ecosystem 😎

---

🪽 Do you like this article? Share it and let it fly! 🛸

💙 *If you enjoy my articles, consider [following me on Twitter](https://twitter.com/eugeniyoz), and/or [subscribing](https://medium.com/@eugeniyoz) to receive my new articles by email.*

---

{% embed https://twitter.com/eugeniyoz/status/1759212371770245614 %}