Is there a way to integrate Angular NgRx dependent selectors and Observables in a loop without the need to unsubscribe from updates consistently?

We are currently utilizing Angular (v18) NgRx for a large application where actions are firing frequently. A new feature requires us to retrieve an array of values and then call another selector for each item in the array in order to construct a view model. The selector we are invoking acts as an aggregator, pulling data from various parts of the AppState. Due to the extensive amount of data being aggregated, the loading process takes a considerable amount of time. We are unable to use any solution that involves using unsubscribe or complete on the Observable.

Failed Attempt 1

export const selectNewFeatureViewModels = createSelector(
  (state: AppState) => state,
  selectNewFeatureRecords,
  (state, newFeatureRecords) => {
    return newFeatureRecords?.map((newFeatureRecord) => {
      return {
        newFeatureRecord,
        aggregatorRecord: selectAggregatorRecord({
          key: newFeatureRecord.key,
        })(state),
      };
    });
  }
);

A similar issue was discussed on Stack Overflow.
NGRX selectors: factory selector within another selector without prop in createSelector method

Attempt 1 worked initially, but it had the drawback of being dependent on AppState. Any change in the app would trigger this selector unnecessarily, causing issues.


We experimented with creating a Dictionary/Map from the selectAggregatorRecord function, however, each value returned an Observable.

Map<string, Observable<AggregatorRecord>>

We encountered compilation errors while trying to pass parameters into the selector, such as the key.

// broken
export const selectAggregatorMap = (keys: string[]) => createSelector(
  ...keys.map(key => selectAggregatorRecord({key})),
  (value) => value
);

Failed Attempt 2

viewModels: {
  newFeatureRecord: NewFeatureRecord;
  aggregatorRecord: AggregatorRecord;
}[];

ngOnInit(): void {
  this.newFeatureFacade
  .getNewFeatureRecords()
  .pipe(
    tap((newFeatureRecords) => {
      newFeatureRecords.forEach((newFeatureRecord) => {
        this.aggregatorRecordFacade
        .getAggregatorRecord({
          key: newFeatureRecord.key,
        })
        .pipe(
          debounceTime(1000),
          filter(
            (aggregatorRecord) =>
              !!aggregatorRecord?.field1 &&
              !!aggregatorRecord?.field2 &&
              !!aggregatorRecord?.field3
          ),
          map((aggregatorRecord) => {
            return {
              newFeatureRecord,
              aggregatorRecord,
            };
          }),
        ).subscribe((viewModel) => {
          if(this.viewModels?.length < 10){
            this.viewModels.push(viewModel);
            this.changeDetectorRef.markForCheck();
          }
        });
      });
    })
  ).subscribe(() => {
    this.viewModels = [];
    this.changeDetectorRef.markForCheck();
  });
}

Attempt 2 also met some success, but it lacks cleanliness. Keeping all this logic within our components is not ideal. We prefer separating concerns. The nested subscribe calls make us uneasy, particularly when subscribing to more records than needed for display.

getNewFeatureRecords might return hundreds of records even though we only want to show 10.

The Facade classes serve as intermediaries between NgRx elements and Angular components, ensuring all interactions go through these Facades for dispatching actions and selecting store data.

@Injectable({ providedIn: 'root' })
export class NewFeatureFacade {
  constructor(private store: Store) {}

  getNewFeatureRecords(): Observable<NewFeatureRecord[]> {
    return this.store.select(selectNewFeatureRecords);
  }
}

@Injectable({ providedIn: 'root' })
export class AggregatorRecordFacade {
  constructor(private store: Store) {}

  getAggregatorRecord({key}: {key: string}): Observable<AggregatorRecord> {
    return this.store.select(selectAggregatorRecord({key});
  }
}

We explored other NgRx operators as well:

  • toArray
  • mergeAll
  • forkJoin
  • switchMap
  • mergeMap

https://www.learnrxjs.io/learn-rxjs/operators

We are keen on streamlining this process. Can you offer any guidance?

Answer №1

Congratulations on your achievement! Please excuse any errors in my English. I attempted to recreate your scenario accurately, so let's break it down:

AppService:

import { Injectable } from "@angular/core";
import { BehaviorSubject, Observable, Subject } from "rxjs";

@Injectable({ providedIn: 'root' })
export class AppService {
  constructor() {

  }

  getNewFeatureRecords = (): Observable<Array<any>> => new BehaviorSubject<Array<any>>([1, 2, 3])
  getAggregatorRecord = ({ key }: { key: string }): Observable<any> => new BehaviorSubject<any>({ key })
}

AppComponent:

import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { combineLatest, debounceTime, filter, map, Subject, switchMap, takeUntil } from 'rxjs';
import { AppService } from './app.services';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet],
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss'
})
export class AppComponent implements OnInit, OnDestroy {
  private readonly unsubscribe$: Subject<void> = new Subject<void>()

  public viewModels: Array<RecordModel> = new Array<RecordModel>()

  private getNewFeatures = () => this._appService.getNewFeatureRecords()
  private getAggregatorState = ({ key }: { key: string }) => this._appService.getAggregatorRecord({ key })

  constructor(
    private _appService: AppService,
    private _cdr: ChangeDetectorRef
  ) {

  }


  ngOnInit(): void {
    this.getNewFeatures().pipe(
      map(el => el.map(el => this.getAggregatorState({ key: el }))),
      map(el => el.map(el => el.pipe(
        debounceTime(1000),
        filter(el => !!el),
        map((el, index) => new RecordModel(index, el)),
        map(model => {
          console.log(model)
          if (this.viewModels.length < 10) { this.viewModels.push(model); this._cdr.markForCheck() }
          return model
        })
      ))),
      // combineLatest(el) – In case you need a set of accumulated values. Maybe it will be useful for building connections
      // merge(...el) – if the order of emit values ​​is not important to you
      switchMap((el) => combineLatest(el)),
      map(el => {
        this.viewModels = []
        this._cdr.markForCheck()
      }),
      takeUntil(this.unsubscribe$)
    ).subscribe()
  }

  ngOnDestroy(): void {
    this.unsubscribe$?.next()
    this.unsubscribe$?.complete()
  }
}

export class RecordModel {
  newFeatureRecord: any
  aggregatorRecord: any

  constructor(newFeatureRecord: any, aggregatorRecord: any) {
    this.newFeatureRecord = newFeatureRecord
    this.aggregatorRecord = aggregatorRecord
  }
}

In the above example, I have consolidated your data streams into a single subscription. Additional logic can be added in each .pipe() to perform calculations for respective observables.

If you require all emitted values from child observables, utilize the combineLatest operator. This will provide an array with results from internal observables in the pipeline.

Alternatively, if emission order is unimportant, consider using the merge() operator.

I trust that I comprehended your issue correctly and aided in its resolution.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

How can you reposition a component within the dom using Angular?

Just started learning Angular, so I'm hoping this question is simple :) Without getting too specific with code, I could use some guidance to point me in the right direction. I'm currently developing a small shopping list application. The idea i ...

What is the process for configuring vue.config.js with typescript?

Just starting out with typescript and running into an issue while configuring vue.config.js const webpack = require("webpack"); module.exports = { plugins: [ new webpack.DefinePlugin({ __VUE_I18N_FULL_INSTALL__: true, __ ...

Creating React Context Providers with Value props using Typescript

I'd prefer to sidestep the challenge of nesting numerous providers around my app component, leading to a hierarchy of provider components that resembles a sideways mountain. I aim to utilize composition for combining those providers. Typically, my pro ...

Finding the imported function in Jest Enzyme's mount() seems impossible

I'm currently facing an issue where I need to mount a component that utilizes a function from a library. This particular function is utilized within the componentDidMount lifecycle method. Here's a simplified version of what my code looks like: ...

Interacting with Angular 2 Components Inside <router-outlet>

In the header component, there is a search bar. Below that, within the same component, there is a "router-outlet". After pressing enter in the search bar (input field), the search string (event.target.value) should be sent to the component inside the rou ...

Test fails in Jest - component creation test result is undefined

I am currently working on writing a Jest test to verify the creation of a component in Angular. However, when I execute the test, it returns undefined with the following message: OrderDetailsDeliveryTabComponent › should create expect(received).toBeTru ...

Utilizing Pipe and Tr in Reactive Forms to Enhance Functionality

I'm facing an issue with my reactive forms when trying to write the pipe inside a tr tag. Here is the code snippet: <tr *ngFor="let row of myForm.controls.rows.controls; "let i = index" [formGroupName]="i"| paginate: { itemsPerPage: 3, currentPage ...

Passing a click event to a reusable component in Angular 2 for enhanced functionality

I am currently working on abstracting out a table that is used by several components. While most of my dynamic table population needs have been met, I am facing a challenge with making the rows clickable in one instance of the table. Previously, I simply ...

Using Angular to send all form data to the API endpoint

Please bear with me as I am new and may ask small or incorrect questions. <form > <label for="fname">First name:</label> <input type="text" id="fname" name="fname"><br><br> <label for="lname">Last name:< ...

Issue with Angular: Unable to bind to 'ngForFrom' as it is not recognized as a valid property of 'a'

I seem to be encountering a recurring error that I can't seem to resolve, despite trying various solutions. Below is the content of my appModule.ts file: import { CommonModule } from '@angular/common'; import { NgModule } from '@angula ...

Encountered a typing issue with the rowHeight property in Angular Material

Utilizing angular and angular material, I'm creating a mat-grid-list using the following code: template <mat-grid-list cols="2" [rowHeight]="rowHeight | async"> component rowHeight = this.breakpointObserver.observe(Breakp ...

Exploring Typescript for Efficient Data Fetching

My main objective is to develop an application that can retrieve relevant data from a mySQL database, parse it properly, and display it on the page. To achieve this, I am leveraging Typescript and React. Here is a breakdown of the issue with the code: I h ...

Unable to capture the exception while using AngularFireAuth

When attempting to handle exceptions with AngularFireAuth.createUserWithEmailAndPassword to display a username/password error message if it already exists, the exception handling code executes properly. However, a similar Exception still manages to bubble ...

Having difficulties executing the npm command to install @ionic/cli-plugin-angular@latest with --save-dev and --save-exact flags on Ionic version 3.2.0

I am in the process of setting up a new project and I am using: ionic version 3.2.0 Cordova CLI: 8.0.0 Node 12.13.1 (lts) Running on Windows 10 When attempting to add a platform with the command ionic cordova platform add <a href="/cdn-cgi/l/email-pro ...

How to pass a function parameter as a property in MongoDB and Typescript

I've been working on a project that involves using Mongoose to write, update, and perform other operations in a MongoDB database. Currently, I am utilizing the updateOne() function within my own custom function. However, I am facing an issue where if ...

Error: XYZ has already been declared in a higher scope in Typescript setInterval

I've come across an interesting issue where I'm creating a handler function and trying to set the current ref to the state's value plus 1: const useTimer = () => { const [seconds, setSeconds] = useState(0); const counterRef = useRef(n ...

Autoformatting files with ESLint on save

I'm encountering an issue where Visual Studio Code is saving my file in violation of the rules specified in my eslint configuration when using eslint and prettier for formatting. module.exports = { env: { browser: true, es2022: true, nod ...

Creating a Jasmine test for the event.target.click can be accomplished by defining a spec that

I need help creating a Jasmine test spec for the following method in my component. Here is my Component Method methodName(event): void { event.preventDefault(); event.target.click(); } I have started writing a test but don't fully cover event. ...

How to focus on an input element in Angular 2/4

Is there a way to focus on an input element using the (click) event? I'm attempting to achieve this with the following code, but it seems like there may be something missing. (I am new to Angular) sTbState: string = 'invisible'; private ele ...

What steps are involved in setting up a Typescript-based custom Jest environment?

Currently, I am attempting to develop an extension based on jest-node-environment as a CustomTestEnvironment. However, I encountered an error when trying to execute jest: ● Test suite failed to run ~/git/my-application/tests/environment/custom-test ...