非规范化ngrx存储设置选择器? [英] Denormalizing ngrx store- setting up selectors?

查看:69
本文介绍了非规范化ngrx存储设置选择器?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我目前正在ngrx项目中使用某种复杂(较深)的结构.可以将其视为具有多个子对象级别的父对象数组.它在服务器端已标准化/扁平化,而我商店中的功能如下所示:

I am currently working with a somewhat complicated (deep) structure within an ngrx project. It can be thought of as an array of parent objects, with multiple levels of child objects. It is normalized/flattened on the server side, and my the feature within my store looks something like this:

rootObjs: {
    level1: {
        byId: {
            'lvl1_1': {id: 'lvl1_1', label: '[Lvl 1]: 1', ui_open: true, children: ['lvl2_1', 'lvl2_3']},
            'lvl1_2': {id: 'lvl1_2', label: '[Lvl 1]: 2', ui_open: false, children: ['lvl2_2']}
        },
        allIds: [
            'lvl1_1', 'lvl1_2'
        ]
    },
    level2: {
        byId: {
            'lvl2_1': {id: 'lvl2_1', label: '[Lvl 2]: 1', ui_open: false, children: ['lvl3_1', 'lvl3_2']},
            'lvl2_2': {id: 'lvl2_1', label: '[Lvl 2]: 2', ui_open: true, children: ['lvl3_3']},
            'lvl2_3': {id: 'lvl2_1', label: '[Lvl 2]: 3', ui_open: false, children: []}
        },
        allIds: [
            'lvl2_1', 'lvl2_2', 'lvl2_3'
        ]
    },
    level3: {
        byId: {
            'lvl3_1': {id: 'lvl3_1', label: '[Lvl 3]: 1', ui_open: false,},
            'lvl3_2': {id: 'lvl3_2', label: '[Lvl 3]: 2', ui_open: false,},
            'lvl3_3': {id: 'lvl3_3', label: '[Lvl 3]: 3', ui_open: false,},
        }
        allIds: [
            'lvl3_1', 'lvl3_2', 'lvl3_3'
        ]
    }
}

现在,我正在尝试编写选择器.我的问题是所有对象都必须立即显示在屏幕上,但是它们必须都可以单独编辑.因此,我正在尝试创建一个选择器,使我可以单独选择每个组件-类似于:

Now I am trying to write my selectors. My issue is that all objects need to be displayed on the screen at once, however they must all be editable separately. Thus, I am trying to create a selector that allows me to select each component individually- something like:

export const rootObjFeature = createFeatureSelector<RootObj>('rootObjs');
export const selectLevel1 = (id: string) => createSelector(
    rootObjFeature, (state: JobPlanner) => {
        // Grab only the level2 children associated with selected level1
        const lvl2 = state.level1.byId[id].children.map(key => state.level2.byId[key]);

        // Grab only the level3 children of level2 associated with selected level1
        const lvl3 = [].concat(
            ...state.lvl2.map( l2 => l2.children.map(key => state.level3.byId[key]));
        );
        return {
            ...state.level1.byId[id],
            level2: lvl2,
            level3: lvl3
        };
    }
);

然后在我的Level1Component初始化中,执行以下操作:

Then in my Level1Component init, I do something like this:

export class Level1Component implements OnInit, OnDestroy {
    @Input() id: string;
    lvl1Sub: Subscription;
    lvl1: Level1Model;

    constructor(private store: Store<AppState>) { }
    ngOnInit() {
        this.lvl1Sub = this.store.select(selectLevel1(this.id)).subscribe(l1 => {
            console.log('loading level 1: '+this.id);
            this.lvl1 = l1;
        });
    }

    ngOnDestroy() {
        this.lvl1Sub.unsubscribe();
    }
}

通过这种设置,我可以将正确的level2level3对象传递到它们自己的组件上(可以在其中打开,关闭,编辑这些子对象等).但是,由于我有选择器,因此无论何时编辑任何level1level2level3项目(例如-将ui_open切换为lvl1_1),每个level1组件的lvl1Sub方法被调用.这是一个问题,因为我的视图可能有数百个level1组件,但是一次只能编辑一个.有没有一种方法可以设置选择器,使其仅在与单个ID相关联的那些存储元素发生更改时才调用其订阅?

With this setup I can pass the proper level2 and level3 objects on to their own components (where those children can be opened, closed, edited, etc..). HOWEVER, due to how I have my selector, any time ANY level1, level2, or level3 item is edited (e.g.- ui_open is toggled for lvl1_1), EVERY level1 component's lvl1Sub method is called. This is an issue as my view might have hundreds of level1 components, but only one will be edited at a time. Is there a way to set up a selector that will only call its subscription when just those store elements associated with a single ID is changed?

推荐答案

我想知道同样的事情.我认为问题在于您想观察数组(Level2s)的过滤子集(特定Level1的子级)而不观察整个数组.但是,据我了解,整个数组(所有Level2s)都是ngrx公开观察的内容以及适用于什么记忆的东西.

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.

想到了三种解决方案.

第一个是更改您的数据模型,以便将给定级别的子级保存在自己的数组中.从本质上讲,这意味着将级别嵌套在状态中.如果您确实具有树结构(子级只有一个父级)而不是图结构(子级有多个父级),那么这可以工作.不过,保持状态扁平化是最佳做法( https://redux. js.org/recipes/structuring-reducers/normalizing-state-shape ).

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).

第二种解决方案是在更精细的级别进行订阅.与其创建带有嵌套对象的顶级对象,不如将每个实体的ID传递给它下面的组件,并且该组件将订阅其自己的状态片.然后,将仅通知与该状态片及其祖先关联的组件.

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.

第三个选项是执行您自己的记忆形式(def:在接收相同的参数时返回最后的结果).使用createSelector的问题在于,它仅查看数组的引用(例如,Level2s的列表)并发现其已更改.您需要一种更深的记忆形式,该记忆形式可以比较切片中您要关注的元素内部的引用,以查看它们是否发生了更改.

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:

  • My understanding of your current scenario: https://stackblitz.com/edit/ngrx-projections-v1
  • Solution #2 (granular subscriptions): https://stackblitz.com/edit/ngrx-projections-v3
  • Solution #3 (custom memoization...ish): https://stackblitz.com/edit/ngrx-projections-v2

打开控制台以查看正在发生的情况.它会打印状态更改以及从状态到每个组件的更改.

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

对于#2,我进行了充分的反应,这增加了很多膨胀感.实际上,我通常不这样做.相反,我会将模型从视图传递到需要它的函数中.

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.

对于#3,我编写了一个名为distinctElements()的自定义运算符,它类似于distinctUntilChanged()运算符,但是它比较数组中元素的引用而不是数组本身.这是它的代码.

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)
    )
};

这篇关于非规范化ngrx存储设置选择器?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆