17

Is it possible to use async await inside of an Angular computed signal?

I have tried to play with that but so far i couldn't make it work correctrly (fetching the value, manipulate the value and returning a raw value to the signal computed result and not a promise)

6 Answers 6

9

Update: Since Angular 19 the original answer is no longer recommended:

Use resources instead.

Use resource (for Promises) or rxResource (for Observables) instead.

Original answer for historical purposes:

Update: added generalization of the concept

Update2: Added cancellation to observable variant

This is a very important question. As @kemsky already mentioned you can use an effect to create such signals.

The nicest solution is to encapsulate the creation and management of such signal in a function, like this:

function myBlahSignal(otherSignal: Signal<Something>) {
  const someService = inject(SomeService);
  const resultSignal = signal<SomeResult>(null);

  effect(
    () => {
      if (!otherSignal()) return;

      someService
        .someMethod(otherSignal())
        .subscribe((result) => resultSignal.set(result));
    },
    { allowSignalWrites: true }
  );

  return resultSignal.asReadonly();
}

You can then use such function directly in any component that has the required source signals:

export class MyComponent {
  otherSignal = someOtherSignal();
  myBlah = myBlahSignal(this.otherSignal1);
}

If we look at this pattern fundamentally, we see that we basically implemented a async computed signal, based on observables. We can abstract this concept in a reusable way:

function myBlahSignal(otherSignal: Signal<Something>) {
  const someService = inject(SomeService);

  return asyncObservableComputed(() => {
    if (!otherSignal()) return null;
    return someService.someMethod$(otherSignal());
  });
}

function asyncObservableComputed<T>(
  computation: () => Observable<T> | undefined | null,
): Signal<T> {
  const resultSignal = signal<T>(null);

  effect(
    (onCleanup) => {
      const result$ = computation();
      if (!result$) return;

      const subscription = result$.subscribe((result) => resultSignal.set(result));
      
      onCleanup(() => subscription.unsubscribe());
    },
    { allowSignalWrites: true },
  );

  return resultSignal.asReadonly();
}

And a promise based variant:

function asyncPromiseComputed<T>(
  computation: () => Promise<T> | undefined | null,
): Signal<T> {
  const resultSignal = signal<T>(null);

  effect(
    async () => {
      const result = await computation();
      resultSignal.set(result);
    },
    { allowSignalWrites: true },
  );

  return resultSignal.asReadonly();
}

We can go one step further and make a fully generic asyncComputed that supports Observable, Promise and plain results at the same time:

export function asyncComputed<T>(
  computation: () => Observable<T> | Promise<T> | T | undefined | null,
): Signal<T> {
  const resultSignal = signal<T>(null);

  effect(
    async () => {
      const result = computation();
      const unwrappedResult = await (isObservable(result)
        ? firstValueFrom(result as Observable<T>, { defaultValue: null })
        : result);

      resultSignal.set(unwrappedResult);
    },
    { allowSignalWrites: true },
  );

  return resultSignal.asReadonly();
}
Sign up to request clarification or add additional context in comments.

9 Comments

What are the implications of allowSignalWrites? I know that stylistically it's better to avoid side-effects, but might allowSignalWrites cause it to behave badly, e.g. not evaluate signal updates properly?
It does exactly what it says: it allows you to write to a signal inside the effect. In this case we write to the resultSignal. There are no negative implications of this. In the pattern used here we basically implement a, nicely encapsulated, asynchronous computed signal.
I agree it's a nice encapsulation. I just wondered because it's explicitly discouraged in the Angular docs. But the wording in the docs is a little ambiguous, and maybe by "Avoid using effects for propagation of state changes" they only mean "in response to state changes that have already participated in change detection" such as from an input signal?
Ahh, right. I think the issues that are mentioned in the documentation can occur if you modify any of the sources of the effect trigger in the effect handler. In our case we only modify the encapsulated signal, which we return with .asReadonly, so I don't think there would be any problem.
I updated the asyncComputed implementation to ensure that the value of the new signal is always set asynchronously, by using a Promise for every case. I don't think the previous implementation had issues, but ensuring async makes this even safer (because it will trigger a fresh change detection).
|
5

You should/can not use async/await inside of computed signal.

You can run async code independently and then set/mutate/update signal or you can use effect function which accepts async functions.

I would recommend reading more about signals and effects here: https://www.angulararchitects.io/aktuelles/angular-signals/.

2 Comments

You're not saying why you shouldn't use async/await. Is it because angular is not able to track which signals are used if they're async? I would assume only the signals used up until the first await are tracked? But that's just a guess.
As far as I know computed() simple not accept async functions.
1

I don't like effect() in it's current state because it decreases the readability of a component IMO. Quite sadly computed() doesn't work with async functions.

The best thing for this typical usecase for me is to use rxjs / toObservable:

  memberId= input.required<number>();
  memberData = signal<MemberData | undefined>(undefined);
  
  constructor(private mySrc: MemberService) {
    toObservable(this.memberId).pipe(takeUntilDestroyed())
    .subscribe(async (id: number)=> 
      this.memberData.set(await this.mySrv.get(id))
    );
  }

2 Comments

even considering that this is a matter of taste, in this code I see essentially the same effect but expressed more verbosely & with additional dependencies
@АнтонЕфанов fair point, but there is a way to use RxJS interop to do it in a side effect free way, see stackoverflow.com/a/79118556/531232
1

I don't know if this will help anyone. I created the following utilities...

export function effectAsync(fn: () => Promise<void>) {
  return effect(() => void (async () => {
    await fn();
  })(), { allowSignalWrites: true });
}

export function computedAsync<T>(initialValue: T, fn: () => Promise<T>) {
  const $result = signal<T>(initialValue);
  effectAsync(async () => {
    $result.set(await fn());
  });
  return $result;
}

... and use them like this...

effectAsync(async () => {
  /* await something */
});


readonly $mySignal = computedAsync<MyType>([], async () => {
    /* return something */
});

Comments

0

Short answer: You can't. The computed function only works for derived values that can be computed synchronously.

The best workaround I found for this problem is to use RxJS interop, like so:

const myItemId = signal<number | undefined>(undefined);
const myItem = toSignal(toObservable(mySelectedItemId).pipe(
    filter((id) => typeof id === 'number'),
    switchMap(async (id) => await fetchItemFromBackend(id))
);

Could be abstracted into a helper function:

function asyncComputed<T, R>(src: Signal<T>, computeFn: (srcVal: T) => Promise<R>): Signal<R | undefined> {
  return toSignal(toObservable(src).pipe(switchMap(computeFn)));
}

Usage:

readonly myItemId = input.required<number>();

readonly myItem = asyncComputed(this.myItemId, 
    async (id) => await fetchItemFromBackend(id));

I prefer this over the effect workaround because it is side-effect free. Also, changing signal values in effect callbacks is strongly discouraged as it can invites nasty bugs such as recursion loops.

I am hoping that future versions of Angular will provide a more direct way to solve these kinds of problems.

Comments

0

For anyone like me landing here searching for an easy way to use a promise in a computed() call, you can simply wrap it in a resource() without a request parameter:

const res = resource({ loader: () => prom });
const comp = computed(() => res.value())

res.value() will first evaluate to undefined and then to the Promise's value as soon as it's resolved.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.