Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create computed signal from signal and observable?

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.
like image 701
George Knap Avatar asked Dec 06 '25 22:12

George Knap


1 Answers

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.

like image 137
Daniel Gimenez Avatar answered Dec 09 '25 14:12

Daniel Gimenez