ngrxngrx-component-storecomponent-store

ngrx Component store with angular, get observable with values from effect?


I try to do a custom autocomplete component in my angular 16 project. That component should be able to lazy fetch the data from my component store.

I achieve to do something like that :

 // MySearchComponent
  @Input({ required: true }) searchValues$?: Observable<SearchInputObject[]>;

  @Input({ required: true }) searchValuesSubscription!: any;// the trigger

  @Input({ required: true }) fieldCtrl!: FormControl<SearchInputObject | null>; // form control to return the selected value

With searchValuesSubscription a trigger of my effect, and searchValues$ the select associated in the store.

I find that solution not very developper friendly, as if I use the component 10 times, I'll have to create those 2 properties in the parent component to use MySearchComponent

ngOnInit() {
  this.searchValues$ = this._myStore.companyCategories$;
  this.searchValuesSubscription$ = this._myStore.loadCompanyCategories;
}

And to call the component :

<app-async-search-input
  [fieldCtrl]="docCategoryCtrl"
  [searchValues$]="searchValues$"            
  [searchValuesSubscription]="searchValuesSubscription$">
</app-async-search-input>

An extract of the Effect, just in case it helps :)

 readonly loadCompanyCategories = this.effect<void>(trigger$ =>
    trigger$.pipe(
      exhaustMap(() =>
        combineLatest([this.userInstance$, this.selectedCompanyId$]).pipe(
          switchMap(([instance, selectedCompanyId]) => {
            if (instance && selectedCompanyId) {
              return this._apiService.getCompanyCategories(instance, selectedCompanyId).pipe(
                tapResponse(
                  companiesCategories => {
                    this.patchState({
                      nonPersistent: {
                        ...this.get().nonPersistent,
                        companyCategories: companiesCategories,
                      },
                    });
                  },
                  (error: HttpErrorResponse) => console.error(error)
                )
              );
            }
            return [];
          })
        )
      )
    )
  );

Here a stackblitz that show a simplified version of what I did : https://stackblitz.com/edit/stackblitz-starters-frdx5b?file=src%2Fapp%2Fasync-autocomplete%2Fasync-autocomplete.component.ts You need to click on "Load books" so the AsyncAutocompleteComponent trigger the store, and then display the list of book in a select.

Is there a way to have a trigger that return on observable of the values ? Or is it something that shouldn't be done with an Effect ? Is there another way to do that ? I'm open to all advice ! Thanks !


Solution

  • Ok, here is a forked stackblitz that shows the correct way to handle event and data flow.

    As I mentioned in my comment, passing observables and subscriptions into components is an anti-pattern. Doing this should be avoided unless absolutely necessary.

    I am going to just post the pieces of code that are different than what you show above...

    When you need to trigger something in a parent component, use an @Output, rather than passing a subscription.

      @Input({ required: true }) searchValues: Book[] | null = [];
    
      @Input({ required: true }) fieldCtrl!: FormControl<any | null>; // form control to return the selected value
    
      @Output() loadBooks = new EventEmitter();
    

    Then this component should handle the async binding for the data, so that change detection is run for the async-autocomplete component whenever the data changes. Also, the there is a new event handler for when the button is pressed, which calls the load in the store.

    <p>Select a book :</p>
    <app-async-autocomplete
      [searchValues]="searchValues$ | async"
      (loadBooks)="onLoadBooks()"
      [fieldCtrl]="fieldCtrl"
    ></app-async-autocomplete>
    

    The load function effect you wrote also needed some adjustments. There really is no need to use an exhaustMap and then a switchMap. I recommend using either combineLastestWith from rxjs or the concatLatestFrom found in the @ngrx/operators library when you need to unwrap observables while processing an effect.

      readonly loadBooks = this.effect<void>((trigger$) =>
        trigger$.pipe(
          tap((_) => console.log('fetch books')),
          combineLatestWith(this.userInstance$, this.selectedCompanyId$),
          switchMap(([_, instance, selectedCompanyId]) => {
            if (instance && selectedCompanyId) {
              return this._booksHttpService.getBooks().pipe(
                tapResponse(
                  (newBooks) => {
                    console.log('newBooks=');
                    console.table(newBooks);
                    this.patchState({
                      books: newBooks,
                    });
                  },
                  (error: HttpErrorResponse) => console.error(error)
                )
              );
            }
            return [];
          })
        )
      );
    

    Please see the stackblitz to see how it all works together.