#how and when am i supposed to read input signal?

23 messages · Page 1 of 1 (latest)

maiden notch
#

i'm making a little page that is supposed to display browser extensions.

here is the service responsible for getting data from a json file:

  constructor(private http: HttpClient) {}

  getData(): Observable<Extension[]> {
    return this.http.get<Extension[]>("data.json");
  }
  

here is the parent component called main that uses the service:

  extensions: Extension[] = [];
  constructor(private dataService: Data) {

  }

  ngOnInit() {

    this.dataService.getData().subscribe((result)=> {
      this.extensions = result;
    })

  }

to test if i was getting the data, i had a method with "console.log(this.extensions)" and a button calling a method, and i got the data, so all is well here.

now i want to give this data to child component called list. so in main html i have this:
<app-list [data] = "extensions"></app-list>

and in list.ts i have this:

  data = input<Extension[]>()
  extList: Extension[] | undefined = [];


  ngOnInit(){
    this.extList = this.data();
  }

when i try to console.log(this.extList) here, i am getting an empty array.
i am thinking it's because i am reading the input signal before the data is actually received but i thought ngOnInit is called after all the components are constructed and data exchanged so i don't know what to do.

hallow hedge
#

It's not related to the child component but the parent one: the extensions are retrieved asynchronously so the data might not be available when the parent component template is initialized.

#

You'd better wrap the child selector in parent template into a condition , only to display it when data is retrieved

maiden notch
rain flame
maiden notch
#

21

rain flame
#

In that case, any chance I can convince you to do this more declaratively? Angular has a ton of tooling precisely so you do not unnecessarily subscribe and cause potential memory leaks or change detection issues

#

In general:

export class ExtensionService {
  private readonly http = inject(HttpClient);
  private readonly data$ = this.loadExtensions().pipe(shareReplay(1));
  public readonly data = toSignal(this.data$);
  
  
  private loadExtensions(){
    return this.http.get<Extension[]>("data.json");
  }
}

Boom, you can now access data anywhere as a signal rather than do weird rigmarole around subscribing yourself directly and assigning to a property (which is an anti-pattern in general).

Your list.ts also should not do the song and dance of this.extList = this.data() - ever

You should have data that can change over time (which includes changing from "undefined" to "I have a value now") always in a signal or observable (or at worst a Promise) so you can trigger change detection correctly and have the template update correctly.

#

In your case, the data input is what you want, why would you try to pull the value out of there instead of just using data()?

#

The assignment rigmarole is precisely the root of your error.
ngOnInit only runs ever once, so in your current code, you take the first value that the data input in list.ts has and assign it to extList.

ngOnInit then never ever gets run again so extList never ever gets updated again.
So when a new value arrives because your ExtensionService finally has the value from data.json, it updates your extensions property in your parent component (mind you this is still an antipattern the way it is written) and that new value is handed over to data, but from there it is not propagated to extList because ngOnInit will not run again.

This is a problem you wouldn't have if you just deleted the extList property outright and used your data input directly.

hallow hedge
rain flame
#

If you do that, contemplate using extensions?.length in case extensions can ever be undefined

maiden notch
#

oohhh okay i think i got it now, thanks a lot

maiden notch
rain flame
#

Neat little example to demonstrate this:
What do you believe happens in this constructor regarding HTTP requests?
What do you believe would happen regarding HTTP requests if you were to delete the constructor?

private readonly http = inject(HttpClient);
private readonly req$ = http.get<SomeData>('someurl');

constructor(){
  this.req$
    .pipe(takeUntilDestroyed())
    .subscribe(resp => console.log("Subscription1", resp);

  this.req$
    .pipe(takeUntilDestroyed())
    .subscribe(resp => console.log("Subscription2", resp);
}
maiden notch
rain flame
#

A cold observable is essentially a factory for something to happen, that is dormant by default.
It gets activated once per subscription.

Therefore, in the above code, with the constructor you will get 2 http requests

#

Because every subscription will trigger the cold observable (which is the http request observable) to be triggered anew to execute entirely from scratch

#

So if you used data$ again somewhere else and subscribed to it, you would fire another HTTP request, which you would not want, since you'd want all places to have the same value as data (which is also subscribing to data$, it's just hidden as part of toSignal).

#

You can prevent that kind of problem by combating the nature of a cold observable here via shareReplay.
What it does is 2 things:

  1. It changes how this cold observable works. Instead of being a factory for http requests it now turns into just being a singular http request that is shared between everybody that subscribes to this observable.
    So when another place subscribes to it, they will not trigger a new http request and if a response comes in, it will be the same response triggering all subscribers at the same time.

However, if you subscribe after that HTTP request is finished, you'd not get be able to access the response anymore. The response already was emitted some time before you subscribed and it's not stored anywhere, so you'd just lose out there.. That's where the second effect comes in:
2) It stores replays the last N values that the observable emitted. So if you subscribe to the obesrvable of the http request 5 minutes after the http request was fired and received a response, you will still be able to access that value since that observable will emit once upon subscription with the http response.

#

As long as you do not subscribe to data$ anywhere else, what I describe will not matter to you and you'll be fine (as you noticed).

But if you ever do need to subscribe to the same http-observable multiple times and want to have the same value shared by both subscriptions, you'll want share or shareReplay