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.
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.
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
// ...
}
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
}
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.