My problem can be best described by analogy with the selectors example of the ngrx documentation to keep things simple (https://github.com/ngrx/platform/blob/master/docs/store/selectors.md#using-selectors-for-multiple-pieces-of-state).
I use the async pipe to subscribe to certain slices of state which I select using selectors, for instance
this.visibleBooks$ = this.store$.select(selectVisibleBooks)
The thing is that, if the allBooks array is "small", <100 items, my view gets updated instantly. But when it is large, >100, my view gets only updated next time change detection is triggered, for instance by scrolling. This is quite a bad user experience, to only see books once you scroll the list.
I looked at the source for async pipe (https://github.com/angular/angular/blob/master/packages/common/src/pipes/async_pipe.ts), and indeed the _updateLatestValue method calls ChangeDetectorRef.markForCheck(), which as far as I understand marks the component to be checked for changes the next time change detection is triggered.
My current way around this is by subscribing manually within the top-level component
this.store$.select(selectVisibleBooks).subscribe(cb)
and calling ChangeDetectorRef.detectChanges() manually within the callback.
I find this however unsatisfactory and would simply like async pipe to always work, no matter how large the Book[] array. Does anybody have some suggestions or a correction with which I could make things work?
edit as per request
The "books store" case above, as said, was just an analogy for the app I'm writing to keep things simple. In reality, my app renders nodes and edges of a graph, where nodes and edges also have a version attached, denoted "vnode", which together with "vedge"s span a version tree. So any graph element has its own version tree.
What I am developing currently is a search form, where we send a certain request to the backend, asking it for any nodes which match a certain set of search key/value pairs.
So, those nodes would then be rendered in a component <nodes-list>, which we pass nodes by input binding
<nodes-list [nodes]="nodes$ | async"></nodes-list>
nodes-list has change detection "on push", while the top-level <search> component has default strategy.
nodes$ is set within ngOnInit() as
this.nodes$ = this.store$.select(selectFullNodesList)
selectFullNodesList looks like this:
export const fullNodesSelector = getFullNodesSelector(createSelector(selectSearchState, s => {
if (s.currentId) {
const nodes = s.queries.get(s.currentId).nodes;
if (nodes) {
return [...nodes];
}
}
return null;
}))
export const selectFullNodesList = createSelector(
fullNodesSelector,
(global: GlobalState) => global.data.counts,
createSelector(selectSearchState, s => s.sort),
(nodes, counts, sorting) => {
if (!nodes || !counts || !sorting) return null;
return [...nodes.sort(sorting.sortCbFactory(counts))];
}
)
Let me explain:
getFullNodesSelector(...)I will show below, it sits in a top-level library because we may reuse it in many features. But what it does is, it takes as an argument another selector which points to an array of node & vnode key pairs{key: number, vKey: number}[], and turns that array into an array of nodes with their vnodes attached (see below how).- So as you can see, the selector we pass it selects the state of our
searchfeature, if there is acurrentId, which is the id of the current request to the backend, then we select the nodes which were the result of our current request. s.queriesis a light wrapper around a Javascript object, which allows me easily get/set values, clone, or add new items to a clone. This I find helpful when working with key/value stores in NGRX. Hence thes.queries.get(s.currentId).nodes.global.data.countsis simply a list of how many neighbors each node has. This I want to know because I'd like to sort the nodes list by "count".s.sortis which sorting of the list is currently selected.- Note the use of
sortCbFactory, this factory simply returns the correct callback to pass toArray.sort, but I needcountsto be present in the local scope of the callback because otherwise I wouldn't be able to sort by counts. - So, whenever nodes change (for instance a new version is referenced on the node), counts change (neighbors are added to a node) or sorting changes, the projection function is called, and a new nodes list is emitted.
- Note that we return a fresh array after sorting.
selectSearchState is simply a feature selector
export const selectSearchState = createFeatureSelector<SearchState>('search');
getFullNodesSelector(...) looks like this:
function getFullNodesSelector(keyPairsSelector: MemoizedSelector<object, GraphElementKeyPair[]>): MemoizedSelector<object, INodeJSON<IVNodeJSON>[]> {
return createSelector(
keyPairsSelector,
(s: GlobalState) => s.data.nodes,
(s: GlobalState) => s.data.vnodes,
(pairs, nodes, vnodes) => {
if (!pairs || !nodes || !vnodes) return null;
return pairs.map(pair => ({
...nodes.get(pair.key),
_SUB: {
...vnodes.get(pair.vKey)
}
}));
})
}
Some comments again:
- As you see, we pass a selector which points to an array of
GraphElementKeyPair({key: number, vKey: number}) - We ask the global state for the nodes store and vnodes store
- We map all pairs to a fresh object.
- Note that nodes and edges are again the wrapper object mentioned earlier, which has a
getmethod.
Thus, as we've subscribed to this.nodes$ with the async pipe, each time there is a new event on the stream <nodes-list> should be updated. However, in practice it appears that this depends on the size of INodeJSON<IVNodeJSON>[], and that if the array has length > ~80, we've got to trigger change detection manually by clicking somewhere or scrolling. nodes-list is refreshed automatically, as should be the case, for smaller arrays.
markForCheckof async pipe, which causes the fresh object at the comp. input not to be detected.asyncpipe ? I'm at similar situation want to useasyncpipe instead of subscribing and triggering manual change detection.