Search code examples
javascriptangularngrxngrx-effectsangular-ngrx-data

@ngrx/data - Undoing an optimistic delete, should an UNDO_ONE action revert the changeState?


Stackblitz

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.


Solution

  • 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.