Search code examples
angularngrx

NGRX - an appstate with dynamic sub-sets


I had a simple app state like this for storing a graph of nodes:

export interface AppState {
    auth: AuthState;
    settings: SettingsState;
    router: RouterReducerState<RouterStateUrl>;
    nodes: NodesState; // <-- element under question
    // ...
}

And this stage is already in prod, there are lot's of actions in reducer, everything is fine, but it is only keeping a single set of nodes. Now I'm looking forward to handle multiple containers of nodes (subsets, different independent graph), I already have support for this in a backend, so I'm thinking about something like this:

export interface AppState {
    auth: AuthState;
    settings: SettingsState;
    router: RouterReducerState<RouterStateUrl>;
    nodes: {[containerId: string]: NodesState}; // each container have it's own set
    // ...
}

I already half way through implementation, selectors looks fine (thanks to Params), all actions also now have clear notion about current container id. But when I came down to reducer registration, I've stuck a little bit. How to register the reducer in this case? Either I choose wrong path, or there is a better way, or I just don't know if there are any way to connect it:

StoreModule.forRoot(<ActionReducerMap<AppState, Action>>{
    auth: <ActionReducer<AuthState, Action>>authReducer,
    settings: <ActionReducer<SettingsState, Action>>settingsReducer,
    nodes: <ActionReducer<NodesState, Action>>nodesReducer, //?
    // ...
}, {

May be I need a wrapper-state like ContainersNodeStates where I would have this dictionary, and a wrapper for reducer as well, but it is still unclear how to reuse existing reducer, how I should route action (event) through one reducer to another... There is a dozen of existing actions (events) that it handles and it is intended to keep consistency inside single container. I'm afraid of excessive duplications, and I never been this far deep in NGRX refactoring. So far NGRX perfectly fits to my project, I really want NGRX survive this refactoring.


Solution

  • Finally I managed to solve it. The core point is - reducers can be called manually as a regular function. This makes it possible to make a nested or delegated reducers.

    1. The AppState model is slightly altered to have a dedicated sub-model for list of collections:
    export interface AppState {
        auth: AuthState;
        settings: SettingsState;
        router: RouterReducerState<RouterStateUrl>;
        // nodes: {[containerId: string]: NodesState};
        // history: {[containerId: string]: HistoryState};
        cols: CollectionsState; // NEW
    }
    
    export interface CollectionsState {
        [cid: string]: CollectionState,
    }
    
    export interface CollectionState {
        nodes: NodesState;
        history: HistoryState;
        container: ContainerInfo; // just metadata about this specific collection
        // ...
    }
    
    1. Next step is - to create and register regular reducer (at root level):
        StoreModule.forRoot(<ActionReducerMap<AppState, Action>>{
            auth: <ActionReducer<AuthState, Action>>authReducer,
            settings: <ActionReducer<SettingsState, Action>>settingsReducer,
            // nodes: <ActionReducer<NodesState, Action>>nodesReducer,
            // history: <ActionReducer<HistoryState, Action>>historyReducer,
            cols: <ActionReducer<CollectionsState, Action>>collectionsReducer, // NEW
        }
    
    1. Now, the main trick is - the collectionsReducer implementation: it have duplicated subscription for every action and just delegate the call to corresponding reducer
    function delegateNodesReducer(cid: string, state: CollectionsState, action: action) {
        return {
            ...state,
            [cid]: <CollectionState> {
                ...state[cid],
                nodes: nodesReducer(state[cid].nodes, action), // This is the trick, manually call existing nodesReducer
            },
        };
    }
        
    const _collectionsReducer = createReducer(initial,
        on(nodesEvent.evNodesReset, (state, action) => delegateNodesReducer(action.cid, state, action)),
        on(nodesEvent.evNodesRetrieved, (state, action) => delegateNodesReducer(action.cid, state, action)),
        on(objectEvents.evCommandCreated, (state, action) => delegateNodesReducer(action.cid, state, action)),
        on(objectEvents.evNodeMoved, (state, action) => delegateNodesReducer(action.cid, state, action)),
        on(objectEvents.evObjectCreated, (state, action) => delegateNodesReducer(action.cid, state, action)),
        on(objectEvents.evObjectDeleted, (state, action) => delegateNodesReducer(action.cid, state, action)),
        on(objectEvents.evPropertyChanged, (state, action) => delegateNodesReducer(action.cid, state, action)),
    
    

    And correspondingly redirect selectors when needed & keep all existing reducers to do the rest of the job. The only downside is - duplicated registration of all actions for any sub-reducer.