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
We are keen on streamlining this process. Can you offer any guidance?