29

select 演算子を rxjs の他の演算子と組み合わせて使用​​し、ChangeDetectionStrategy.OnPush の参照整合性を維持するような方法でツリー データ構造 (ストア内でフラット リストに正規化) をクエリする方法を見つけようとしています。セマンティクスですが、私の最善の試みにより、ツリーの一部が変更されたときにツリー全体が再レンダリングされます。誰にもアイデアはありますか?次のインターフェイスをストア内のデータの代表と見なす場合:

export interface TreeNodeState {
 id: string;
 text: string;
 children: string[] // the ids of the child nodes
}
export interface ApplicationState {
 nodes: TreeNodeState[]
}

上記の状態を非正規化して、次のインターフェイスを実装するオブジェクトのグラフを返すセレクターを作成する必要があります。

export interface TreeNode {
 id: string;
 text: string;
 children: TreeNode[]
}
つまり、Observable<ApplicationState> を受け取り、Observable<TreeNode[]> を返す関数が必要です。これにより、各 TreeNode インスタンスは、その子のいずれかが変更されない限り、参照整合性を維持します

理想的には、ノードが変更されたときに完全に新しいグラフを返すのではなく、グラフの一部が変更された場合にのみ子を更新するようにしたいと考えています。ngrx/store と rxjs を使用してそのようなセレクターを構築する方法を知っている人はいますか?

私が試みた種類のより具体的な例については、以下のスニペットをチェックしてください。

// This is the implementation I'm currently using. 
// It works but causes the entire tree to be rerendered
// when any part of the tree changes.
export function getSearchResults(searchText: string = '') {
    return (state$: Observable<ExplorerState>) =>
        Observable.combineLatest(
            state$.let(getFolder(undefined)),
            state$.let(getFolderEntities()),
            state$.let(getDialogEntities()),
            (root, folders, dialogs) =>
                searchFolder(
                    root,
                    id => folders ? folders.get(id) : null,
                    id => folders ? folders.filter(f => f.parentId === id).toArray() : null,
                    id => dialogs ? dialogs.filter(d => d.folderId === id).toArray() : null,
                    searchText
                )
        );
}

function searchFolder(
    folder: FolderState,
    getFolder: (id: string) => FolderState,
    getSubFolders: (id: string) => FolderState[],
    getSubDialogs: (id: string) => DialogSummary[],
    searchText: string
): FolderTree {
  console.log('searching folder', folder ? folder.toJS() : folder);
  const {id, name } = folder;
  const isMatch = (text: string) => !!text && text.toLowerCase().indexOf(searchText) > -1;
  return {
    id,
    name,
    subFolders: getSubFolders(folder.id)
        .map(subFolder => searchFolder(
            subFolder,
            getFolder,
            getSubFolders,
            getSubDialogs,
            searchText))
      .filter(subFolder => subFolder && (!!subFolder.dialogs.length || isMatch(subFolder.name))),
    dialogs: getSubDialogs(id)
      .filter(dialog => dialog && (isMatch(folder.name) || isMatch(dialog.name)))

  } as FolderTree;
}

// This is an alternate implementation using recursion that I'd hoped would do what I wanted
// but is flawed somehow and just never returns a value.
export function getSearchResults2(searchText: string = '', folderId = null)
: (state$: Observable<ExplorerState>) => Observable<FolderTree> {
    console.debug('Searching folder tree', { searchText, folderId });
    const isMatch = (text: string) =>
        !!text && text.search(new RegExp(searchText, 'i')) >= 0;
    return (state$: Observable<ExplorerState>) =>
        Observable.combineLatest(
            state$.let(getFolder(folderId)),
            state$.let(getContainedFolders(folderId))
                .flatMap(subFolders => subFolders.map(sf => sf.id))
                .flatMap(id => state$.let(getSearchResults2(searchText, id)))
                .toArray(),
            state$.let(getContainedDialogs(folderId)),
            (folder: FolderState, folders: FolderTree[], dialogs: DialogSummary[]) => {
                console.debug('Search complete. constructing tree...', {
                    id: folder.id,
                    name: folder.name,
                    subFolders: folders,
                    dialogs
                });
                return Object.assign({}, {
                    id: folder.id,
                    name: folder.name,
                    subFolders: folders
                        .filter(subFolder =>
                            subFolder.dialogs.length > 0 || isMatch(subFolder.name))
                        .sort((a, b) => a.name.localeCompare(b.name)),
                    dialogs: dialogs
                        .map(dialog => dialog as DialogSummary)
                        .filter(dialog =>
                            isMatch(folder.name)
                            || isMatch(dialog.name))
                        .sort((a, b) => a.name.localeCompare(b.name))
                }) as FolderTree;
            }
        );
}

// This is a similar implementation to the one (uses recursion) above but it is also flawed.
export function getFolderTree(folderId: string)
: (state$: Observable<ExplorerState>) => Observable<FolderTree> {
    return (state$: Observable<ExplorerState>) => state$
        .let(getFolder(folderId))
        .concatMap(folder =>
            Observable.combineLatest(
                state$.let(getContainedFolders(folderId))
                    .flatMap(subFolders => subFolders.map(sf => sf.id))
                    .concatMap(id => state$.let(getFolderTree(id)))
                    .toArray(),
                state$.let(getContainedDialogs(folderId)),
                (folders: FolderTree[], dialogs: DialogSummary[]) => Object.assign({}, {
                    id: folder.id,
                    name: folder.name,
                    subFolders: folders.sort((a, b) => a.name.localeCompare(b.name)),
                    dialogs: dialogs.map(dialog => dialog as DialogSummary)
                        .sort((a, b) => a.name.localeCompare(b.name))
                }) as FolderTree
            ));
}

4

1 に答える 1