Denormalizing ngrx store- setting up selectors?

半城伤御伤魂 提交于 2019-12-05 22:56:43

I have wondered the same thing. I think the issue is that you want to observe a filtered subset (children of specific Level1) of an array (Level2s) without observing the entire array. However, in my understanding, the entire array (all Level2s) is what ngrx exposes for observing and what memoization is applied to.

Three solutions come to mind.

The first is to change your datamodel so that the children of a given level are held in their own array. This would essentially mean nesting your levels in your state. If you truly have a tree structure (child only has one parent) rather than a graph structure (child has multiple parents) then this could work. However keeping your state flate is the best practice (https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape).

The second solution is to subscribe at a more granular level. Instead of creating a top level object with nested objects under it you could just pass the id of each entity to the component below it and that component would subscribe to its own slice of state. Then only the component associated with that slice of state and its ancestors will be notified.

The third option is to do your own form of memoization (def: return the last result when receiving the same arguments). The problem with using createSelector is that it just looks at the reference of the array (list of Level2s for instance) and sees that it changes. You need a deeper form of memoization that compares the references of the elements inside of the slice that you care about to see if they changed.

The poor man's version is to setup your own distinct filter before materializing your model at the end of your projection. The basic gist is that you filter the list of children to only what you want, apply a pairwise operator so that you can know what you got last time, and then filter the stream to ignore values where the references of the objects inside of the current and previous emit are the same.

Here are some running examples:

Open up the console to see what is happening. It prints state changes and changes to each component from state.

For #2 I went full reactive which adds a good bit of bloat. In practice I usually don't do that. Rather I would pass the model from the view into the functions that need it.

For #3 I wrote a custom operator called distinctElements() which is like the distinctUntilChanged() operator but it compares the references of the elements within an array rather than the array itself. Here is the code for it.

import { Observable } from 'rxjs/Observable';
import { startWith, pairwise, filter, map } from 'rxjs/operators';

export const distinctElements = () => <T>(source: Observable<T[]>) => {
    return source.pipe(
        startWith(<T[]>null),
        pairwise(),
        filter(([a, b]) => a == null || a.length !== b.length || a.some(x => !b.includes(x))),
        map(([a, b]) => b)
    )
};

Rerendering the entire UI might not be as costly as you think as long as you adhere to angular best practices (make sure you specify the trackBy key).

If you are still concerned about it, then you could split out the ids from the details and only use the ids for rendering the list.

arr = [1,2,3,4.....]
<div *ngFor="let id of arr"><sub-component [itemId]="id"></subcomponent></div>

Then inside of your sub component you could use the store to select the details for each component utilizing the provided input.

_itemId = null
@input()
set itemId(value) {
    if (value !== this._itemId) {
        this._itemId = value
        this.details = this.store.select(selectDetails(value))
    }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!