In @ngrx/data doing an optimistic delete (of our Hero "Ant-Man") causes changeState to be updated as shown below:
{
"entityCache": {
"Hero": {
"ids": [1, 2, 3, 5, 6],
"entities": {
"1": {
"id": 1,
"name": "Spiderman",
"power": 1
},
"2": {
"id": 2,
"name": "Thor",
"power": 5
},
"3": {
"id": 3,
"name": "Hulk",
"power": 6
},
"5": {
"id": 5,
"name": "Iron Man",
"power": 9
},
"6": {
"id": 6,
"name": "Thanos",
"power": 10
}
},
"entityName": "Hero",
"filter": "",
"loaded": true,
"loading": true,
"changeState": {
"4": {
"changeType": 2,
"originalValue": {
"id": 4,
"name": "Ant-Man",
"power": 7
}
}
}
}
}
}
Using the effect below I've fired an UNDO_ONE when the delete fails due to a http request error:
deleteError$ = createEffect(() => {
return this.actions$.pipe(
ofEntityType("Hero"),
ofEntityOp([EntityOp.SAVE_DELETE_ONE_ERROR]),
map(action => {
const id = action.payload.data.originalAction.payload.data;
const options: EntityActionOptions = {
// tried various values
}
return new EntityActionFactory().create( <-----------------------dispatch UNDO_ONE action-----------
"Hero",
EntityOp.UNDO_ONE,
id,
options
);
})
);
});
Question: Should dispatching an UNDO_ONE action revert the changeState
i.e. remove the changes to this part of the entities state caused by a delete action?
If so, how do you correctly dispatch an UNDO_ONE and what arguments are required?
I've explored different values for both data and options for the EntityActionFactory.create() method:
EntityActionFactory.create<P = any>(entityName: string, entityOp: EntityOp, data?: P, options?: EntityActionOptions): EntityAction<P>
Here I'm doing an optimistic delete and on a SAVE_DELETE_ONE_ERROR dispatching an UNDO_ONE action via an effect.
When I swap out UNDO_ONE for UNDO_ALL changeState
does revert back to {}
which gives me cause to think changeState
should revert back to {}
given we're cancelling the delete.
According to the documentation here, it should :
The undo operations replace entities in the collection based on information in the changeState map, reverting them their last known server-side state, and removing them from the changeState map. These entities become "unchanged."
In order to overcome this issue, you can create a metaReducer which removes the relevant modifications remaining in the changeState after an undo action. Here is the content of my entity-metadata.ts with the relevant metareducer.
import { EntityMetadataMap, EntityDataModuleConfig, EntityCache } from '@ngrx/data';
import { MetaReducer, ActionReducer, Action } from '@ngrx/store';
const entityMetadata: EntityMetadataMap = {};
const pluralNames = {};
const objectWithoutProperties = (obj, keys) => {
const target = {};
for (const i in obj) {
if (keys.indexOf(i) >= 0) { continue; }
if (!Object.prototype.hasOwnProperty.call(obj, i)) { continue; }
target[i] = obj[i];
}
return target;
};
function revertStateChanges(reducer: ActionReducer<any>): ActionReducer<any> {
return (state, action: any) => {
if (action.type.includes('@ngrx/data/undo-one')) {
// Note that you need to execute the reducer first if you have an effect to add back a failed removal
state = reducer(state, action);
const updatedChangeState = objectWithoutProperties(state[action.payload.entityName].changeState, [action.payload.data.toString()]);
const updatedState = {
...state,
[action.payload.entityName]: {
...state[action.payload.entityName],
changeState: updatedChangeState
}
};
return reducer(updatedState, action);
}
return reducer(state, action);
};
}
const entityCacheMetaReducers: MetaReducer<EntityCache, Action>[] = [revertStateChanges];
export const entityConfig: EntityDataModuleConfig = {
entityMetadata,
pluralNames,
entityCacheMetaReducers
};
There might be a better way of writing this code (in particular the way I handled the override changeState property) but for my case it proved to work.
Moreover, it might need some updates in order to handle the different undo cases, as, when I wrote it I just needed to make it work for an undo concerning a delete action, where action.payload.data is the entity id.