Денормализация ngrx store- настройка селекторов?

В настоящее время я работаю с несколько сложной (глубокой) структурой в рамках проекта ngrx. Его можно рассматривать как массив родительских объектов с несколькими уровнями дочерних объектов. Он нормализован / сплющен на стороне сервера, и моя функция в моем магазине выглядит примерно так:

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'
        ]
    }
}

Сейчас я пытаюсь написать мои селекторы. Моя проблема в том, что все объекты должны отображаться на экране одновременно, однако все они должны редактироваться отдельно. Таким образом, я пытаюсь создать селектор, который позволяет мне выбирать каждый компонент индивидуально - что-то вроде:

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 я делаю что-то вроде этого:

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

С этой настройкой я могу передать правильное level2 а также level3 объекты в их собственных компонентах (где эти дочерние элементы могут быть открыты, закрыты, отредактированы и т. д.). ОДНАКО, из-за того, как у меня есть мой селектор, в любое время level1, level2, или же level3 элемент редактируется (например, ui_open переключается на lvl1_1), КАЖДЫЙ level1 компоненты lvl1Sub метод называется. Это проблема, так как мой взгляд может иметь сотни компонентов уровня 1, но за один раз будет редактироваться только один. Есть ли способ настроить селектор, который будет вызывать свою подписку только при изменении только тех элементов магазина, связанных с одним идентификатором?

2 ответа

Я удивился тому же. Я думаю, что проблема заключается в том, что вы хотите наблюдать отфильтрованное подмножество (потомки определенного уровня 1) массива (уровни 2), не наблюдая весь массив. Однако, в моем понимании, весь массив (все Уровни 2) - это то, что ngrx предоставляет для наблюдения и к чему применяется мемоизация.

На ум приходят три решения.

Во-первых, нужно изменить свою модель данных, чтобы дочерние элементы данного уровня содержались в своем собственном массиве. По сути это будет означать вложение ваших уровней в вашем штате. Если у вас действительно есть древовидная структура (у ребенка есть только один родитель), а не графовая структура (у ребенка есть несколько родителей), тогда это может сработать. Тем не менее, сохранение состояния вашего штата является лучшей практикой ( https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape).

Второе решение - подписаться на более детальном уровне. Вместо создания объекта верхнего уровня с вложенными объектами под ним, вы можете просто передать идентификатор каждой сущности компоненту под ним, и этот компонент подпишется на свой собственный фрагмент состояния. Тогда только компонент, связанный с этим срезом состояния, и его предки будут уведомлены.

Третий вариант - сделать свою собственную форму напоминания (def: вернуть последний результат при получении тех же аргументов). Проблема с использованием createSelector в том, что он просто смотрит на ссылку массива (например, список уровней 2) и видит, что он меняется. Вам нужна более глубокая форма запоминания, которая сравнивает ссылки элементов внутри среза, которые вам нужны, чтобы увидеть, изменились ли они.

Версия бедняка состоит в том, чтобы установить свой собственный отчетливый фильтр, прежде чем материализовать вашу модель в конце вашей проекции. Основная суть в том, что вы фильтруете список дочерних элементов только для того, что вам нужно, применяете парный оператор, чтобы вы могли знать, что вы получили в прошлый раз, а затем фильтруете поток, чтобы игнорировать значения, где ссылки на объекты внутри текущего и предыдущий выброс одинаковы.

Вот несколько запущенных примеров:

Откройте консоль, чтобы увидеть, что происходит. Он печатает изменения состояния и изменения для каждого компонента из состояния.

Для #2 я стал полностью реактивным, что добавляет немало вздора. На практике я обычно так не делаю. Скорее я бы передал модель из представления в функции, которые в ней нуждаются.

Для #3 я написал собственный оператор под названием distinctElements() который похож на distinctUntilChanged() оператор, но он сравнивает ссылки элементов в массиве, а не сам массив. Вот код для этого.

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

Повторная визуализация всего пользовательского интерфейса может быть не такой дорогой, как вы думаете, если вы придерживаетесь лучших рекомендаций (не забудьте указать ключ trackBy).

Если вы все еще обеспокоены этим, то вы можете отделить идентификаторы от деталей и использовать только идентификаторы для рендеринга списка.

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

Затем внутри вашего подкомпонента вы можете использовать магазин, чтобы выбрать детали для каждого компонента, используя предоставленные данные.

_itemId = null
@input()
set itemId(value) {
    if (value !== this._itemId) {
        this._itemId = value
        this.details = this.store.select(selectDetails(value))
    }
}
Другие вопросы по тегам