2

I have an Angular 20 application with a Component that fetches logs from a bookService based on a required Input. The service returns an Observable.

I tried to transform the Observable from the service with toSignal(), like this:

export class HistoryComponent {
  private readonly bookService = inject(BookService);
  public readonly bookId = input.required<string>();

  public logs = toSignal(
    this.bookService.getLogs(this.bookId()),
    {initialValue: []}
  );
}

But this gives me the following error:

RuntimeError: NG0950: Input "bookId" is required but no value is available yet. Find more at https://angular.dev/errors/NG0950
    at _HistoryComponent.inputValueFn [as bookId] (core.mjs:61:19)
    at <instance_members_initializer> (history.component.ts:21:55)
    at new _HistoryComponent (history.component.ts:16:7)

It seems that toSignal() requires a value of the input signal to be available at construction. My options now seem to be as follows:

  1. Use toSignal() in the ngOnInit lifecycle-hook to be sure to have a value for the bookId input signal. But there I have no Injection context available (which is needed for toSignal), so I would need withInjectionContext() which would be a bit hacky.
  2. Make the bookId input not required, and handle empty values to not make the service call. This doesn't feel right, as I can make sure the input is always set by the parent component, so it should be required.
  3. Use effect() instead of toSignal(), see below.

The first two solutions feel very wrong, so I went with the effect() - currently I have the following working solution:

export class HistoryComponent {
  private readonly bookService = inject(BookService);
  public readonly bookId = input.required<string>();

  public logs: BookLogs[] = [];

  public constructor() {
    effect(() => this.bookService.getLogs(this.bookId())
      .subscribe(logs => this.logs = logs));
  }
}

Is this really current best practice? I hope there's a better solution which uses toSignal().

1
  • The obvious answer here is the resource pattern but it requires 19.0 and relies on a still "experimental" API. Commented Aug 25 at 15:30

2 Answers 2

1

We can check the Angular documentation NG0950 - Required input is accessed before a value is set.

Fixing the error:

Access the required input in reactive contexts. For example, in the template itself, inside a computed, or inside an effect.
Alternatively, access the input inside the ngOnInit lifecycle hook, or later.


Looking at this issue NG0952: Input is required but no value is available yet. #55010 and comment from JeanMache

JeanMeche on Mar 23, 2024
Hi, required inputs can only be read after the component is init (after ngOnInit()). By invoking user in the property initialization you're accessing a signal that hasn't been set yet.
In your case it looks like expired should be a computed input.


Reason for Error:

You should not use a toSignal (Since I believe initialization is on a non reactive context - the observable using the signal is initialized in a non reactive context). Since it reads the value before ngOnInit, hence we get the error.

Fix:

Instead we can use rxResource which can be used to asynchronously fetch the data. Which executes the params signals in a reactive context, which gets rid of the error.

@Component({
  selector: 'app-child',
  imports: [CommonModule],
  template: `
  @if(logs.error(); as error) {
    {{error}}
  } @else if(logs.isLoading()) {
    Loading...
  } else {
    {{logs.value() | json}}
  }
  `,
})
export class Child {
  private readonly bookService = inject(BookService);
  public readonly bookId = input.required<string>();

  public logs = rxResource({
    params: () => this.bookId(),
    stream: ({ params: bookId }) => this.bookService.getLogs(bookId),
    defaultValue: [],
  });
}

Full Code:

import { Component, Injectable, inject, input, signal } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { bootstrapApplication } from '@angular/platform-browser';
import { of, delay, Observable } from 'rxjs';
import { CommonModule } from '@angular/common';

@Injectable({ providedIn: 'root' })
export class BookService {
  getLogs(id: any): Observable<any> {
    return of({ test: 1 }).pipe(delay(3000));
  }
}

@Component({
  selector: 'app-child',
  imports: [CommonModule],
  template: `
    @if(logs.error(); as error) {
      {{error}}
    } @else if(logs.isLoading()) {
      Loading...
    } @else {
      {{logs.value() | json}}
    }
  `,
})
export class Child {
  private readonly bookService = inject(BookService);
  public readonly bookId = input.required<string | undefined>();

  public logs = rxResource({
    params: () => this.bookId(),
    stream: ({ params: bookId }) => this.bookService.getLogs(bookId),
    defaultValue: [],
  });
}
@Component({
  selector: 'app-root',
  imports: [Child],
  template: `
    <!-- @if(bookId(); as bookIdVal) { -->
      <app-child [bookId]="bookId()"/>
    <!-- } -->
    <!-- @let bookIdVal2 = bookId() || '';
    <app-child [bookId]="bookIdVal2"/> -->
  `,
})
export class App {
  name = 'Angular';
  bookId = signal<string | undefined>(undefined);

  ngOnInit() {
    this.bookId.set('1');
  }
}

bootstrapApplication(App);

Stackblitz Demo

Sign up to request clarification or add additional context in comments.

2 Comments

Ah, rxResource() seems like the perfect fit and the thing I've been looking for, thanks! A pity that it's still experimental though (even in Angular 20).
@adrianus its almost stable, you can use for new development
0

As mentioned in the above answer, the solution is to ensure, the signal is accessed inside a reactive context.

The problem with the above answer is that the rxResource is still experimental.

To solve this problem in a stable method, all you need to do, is return the observable inside a computed (Reactive context), which ensures the error does not happen.

@Component({
  selector: 'app-child',
  imports: [CommonModule],
  template: `
    {{logs() | async | json}}
  `,
})
export class Child {
  private readonly bookService = inject(BookService);
  public readonly bookId = input.required<string | undefined>();

  public logs = computed(() => this.bookService.getLogs(this.bookId()));
}

Full Code:

import {
  Component,
  Injectable,
  computed,
  inject,
  input,
  signal,
} from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { bootstrapApplication } from '@angular/platform-browser';
import { of, delay, Observable } from 'rxjs';
import { CommonModule } from '@angular/common';

@Injectable({ providedIn: 'root' })
export class BookService {
  getLogs(id: any): Observable<any> {
    return of({ test: 1 }).pipe(delay(3000));
  }
}

@Component({
  selector: 'app-child',
  imports: [CommonModule],
  template: `
    {{logs() | async | json}}
  `,
})
export class Child {
  private readonly bookService = inject(BookService);
  public readonly bookId = input.required<string | undefined>();

  public logs = computed(() => this.bookService.getLogs(this.bookId()));
}
@Component({
  selector: 'app-root',
  imports: [Child],
  template: `
    <!-- @if(bookId(); as bookIdVal) { -->
      <app-child [bookId]="bookId()"/>
    <!-- } -->
    <!-- @let bookIdVal2 = bookId() || '';
    <app-child [bookId]="bookIdVal2"/> -->
  `,
})
export class App {
  name = 'Angular';
  bookId = signal<string | undefined>(undefined);

  ngOnInit() {
    this.bookId.set('1');
  }
}

bootstrapApplication(App);

Stackblitz Demo

1 Comment

This will make logs to be of type Signal<Observable<BookLogs[]>>. I would like to only have Signal<BookLogs[]>.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.