#Is writing a custom useEffect a good idea for signals ?

8 messages · Page 1 of 1 (latest)

patent wagon
#

Somtimes when i write effects i end up having depencies that re run the effect in nested functions i made this so it was easier for me to understand exactly which one will re run the effect has the same footgun as react use effect, this makes working with effects easier, your thoughts ?

import {
  CreateEffectOptions,
  effect,
  EffectCleanupRegisterFn,
  EffectRef,
  Signal,
  untracked,
} from '@angular/core';

// Extract the inner type from a Signal
type SignalValue<T> = T extends Signal<infer V> ? V : never;

// Map a tuple of Signals to a tuple of their value types
type DependenciesValues<T extends readonly Signal<unknown>[]> = {
  [K in keyof T]: SignalValue<T[K]>;
};

export const useEffect = <const T extends readonly Signal<unknown>[]>(
  callback: (...values: DependenciesValues<T>) => void | Promise<void>,
  dependencies: T,
  options: CreateEffectOptions = {},
): EffectRef => {
  return effect(
    async () => {
      const values = dependencies.map((dep) => dep()) as DependenciesValues<T>;

      untracked(() => {
        callback(...values);
      });
    },
    {
      ...options,
    },
  );
};

Here is example usage

export class AppComponent {
  foo = signal('');
  constructor() {
    useEffect(
      (value) => {
        console.log('Use effect ran ', value);
      },
      [this.foo],
    );

    setTimeout(() => {
      this.foo.set('Bar');
      console.log('Efect ran');
    }, 5000);
  }
}
proven bay
#

I'd argue if you need to do that, something is going wrong beforehand, this hints at a code-smell.
If you have code so complex that it requires you to think about your dependencies, you are not splitting things out enough.
To avoid signals getting called in child-functions, make sure you call your signals in your effect and pass their values on to whatever helper function you need.

Separately from that - In your AppComponent example, you could be handling that particular case declaratively anyway and I'd argue it is neater to handle in rxjs.

private readonly fooChanged$ = new Subject<T>();
private readonly foo$ = this.fooChanged$.pipe(
  switchMap(
    foo => timer(5000).pipe(map(() => 'Bar'), startWith(foo))
  )
);
protected readonly foo = toSignal(this.foo$);

protected changeFoo(newVal: T){
  this.fooChanged.next(newVal);
}

In this example, foo$ will get set to whatever new Value runs through fooChanged and always reset back to 'Bar' automatically after 5s.

drowsy tide
#

effect and untracked are primitives that should always be used together. Never run utility functions without untracked, at least that's how i do it. If that's too verbose, it's totally fine to build your own high level wrapper around effect and untracked and even resemble the useEffect style.

Beyond that though, i personally do not favor when input arguments come last. I'd rather see something like

useEffect({
  foo: this.fooSignal,
  bar: () => this.barSignal(),
}, ({ foo, bar }) => {
  // this is untracked
})

and this is not far from the explicit use of low level primitives

effect(() => {
  const foo = this.fooSignal()
  const bar = this.barSignal()
  untracked(() => {
    //
  })
})
vestal crown
#

Effect can conditionally depend on signals. With a static array of dependencies, it loses this capability.

effect(() => {
  if (cond()) {
    signal1();
  } else {
    signal2();
  }
});

If cond() is true, signal2 is never accessed and the effect won't run if it changes.

solar sierra
lofty ridge
solar sierra
#

one is an extension of effect, the other is its own effect, mostly DX differences

lofty ridge