How to create computed signal from signal and observable?
Asked Answered
C

3

11

Imagine standard situation in Angular: You need to fetch data from server for some entity ID. Entity Id is a Signal and you want the fetched data to be signal too.

Something like this feels natural:


@Component({
  selector: 'my-app',
  standalone: true,
  imports: [CommonModule],
  template: `
  <code>{{roles()}}</code>
  `,
})
export class App {
  userId: Signal<string> = signal('userA');
  roles = computed(() => toSignal(getRoles(this.userId())));
}

//this could be api service sending  http request
const getRoles = (userId: string) => {
  return userId === 'userA' ? of([1, 2, 3]) : of([1]);
};

but there is a runtime errror in browser console:

Error: NG0203: toSignal() can only be used within an injection context such as a constructor, a factory function, a field initializer, or a function used with `runInInjectionContext`. Find more at https://angular.io/errors/NG0203

Stackblitz demo here

UPDATE: I also tried to provide Injector into toSignal:

 constructor(private injector: Injector) {}
  userId: Signal<string> = signal('userA');
  roles = computed(() =>
    toSignal(getRoles(this.userId()), { injector: this.injector })()
  );

but then another runtime error:

Error: NG0600: Writing to signals is not allowed in a `computed` or an `effect` by default. Use `allowSignalWrites` in the `CreateEffectOptions` to enable this inside effects.
Chitter answered 7/5, 2023 at 12:54 Comment(3)
The second error is misleading since there is no allowSignalWrites option for computed() method. I checked the source code, but I couldn't find anything suspicious. You should create a GitHub issue for this since it's a really common use case. Also, this is still in the Developer Preview so these issues are expected I guess.Verdaverdant
I see you have posted this issue on the repo : github.com/angular/angular/issues/50192Fowling
Even if this worked as you'd planned you'd end up with Signal<Signal<Roles[]>> and then have to dereference it twice roles()().Kelvin
P
11

There is no need for effect, as what you want can be accomplished with the rxjs interop functions.

For the roles signal, the userId has to be converted to an observable with toObservable. Then value of this new observable is piped to a switchMap operator to get the roles value from a service. Finally, the inner observable stream is converted back to a signal by passing it to toSignal.

export class App {
  readonly userId = signal('userA');

  readonly roles = toSignal(toObservable(this.userId).pipe(
    switchMap(userId => getRoles(userId)),
  ), { initialValue: [] });
}

In the example above, an initial value is provided. You can omit that, but that will create an initial undefined emission.

Parenthood answered 14/9, 2023 at 14:9 Comment(0)
O
4

How about something more like this:

export class App {
  userId = signal('userA');
  roles = signal<number[]>([]);

  roleEffect = effect(() =>
    this.getRoles(this.userId()).subscribe(
       r = this.roles.set(r)
    )
  );
}
Obscure answered 26/5, 2023 at 0:2 Comment(2)
If there would be more signals in this class would this effect be run on all signals changes - so getRoles would be fired every time?Renarenado
The effect always gets the current value of its signals. So if several signals change before the effect is run, it will only run one time using the current value of all signals.Obscure
F
0

I'd say, you have to handle the signal as any async value :

roles = computed(() =>
  getRoles(this.userId())
);

And call the async pipe :

{{roles() | async }

What do you think about it ?


Edit: About the 2nd error message, you're writing a signal by creating it with toSignal() which also subscribes and sets the new values. (See the source code)

Fowling answered 7/5, 2023 at 13:16 Comment(4)
well, interesting idea. In that case I guess I'd prefer to have effect that will create an observable and assign it to class propery. Sounds simpler.Chitter
I'm not sure that's the kind of use cases effect is for.Fowling
You don't really get any benefits from using Signals at all with this way. Also it wouldn't initiate the http call until the template tried to display the value - which seems wrong.Kelvin
This seems to be the correct approach, especially if coming from a rxjs background where you would expect something like public roles$ = userId$.pipe(switchMap(id => getRoles(id))); Your answer looks like the drop in signal replacement for it which is great.Bond

© 2022 - 2024 — McMap. All rights reserved.