From 69470f8680519c68cefc295e288fb4a2845ac2fb Mon Sep 17 00:00:00 2001 From: Remo Date: Thu, 22 Dec 2016 15:00:13 +0100 Subject: [PATCH] #30 Avoid store state updates if nothing changed updated POST_STORE_RESOURCE and PATCH_STORE_RESOURCE to detect if no changes where applied. --- spec/reducers.spec.ts | 38 +++++++++++++- src/reducers.ts | 115 ++++++++++++++++++++++++------------------ src/utils.ts | 30 ++++++----- 3 files changed, 120 insertions(+), 63 deletions(-) diff --git a/spec/reducers.spec.ts b/spec/reducers.spec.ts index e6845e6..c425b0c 100644 --- a/spec/reducers.spec.ts +++ b/spec/reducers.spec.ts @@ -180,7 +180,7 @@ describe('NgrxJsonApiReducer', () => { attributes: { title: 'bla bla bla' } - } + } }, })); expect(newState.data['Article']['1'].resource.attributes.title).toEqual('bla bla bla'); @@ -357,6 +357,42 @@ describe('NgrxJsonApiReducer', () => { expect(newState.data['Article']['1']).toBeDefined(); expect(newState2.data['Article']['1']).toBeDefined(); }); + + it('should not update state on second identical post', () => { + let action = new PostStoreResourceAction( + { type: 'Article', id: '1', attributes : {title : 'sample title'} } + ); + let newState = NgrxJsonApiStoreReducer(state, action); + let newState2 = NgrxJsonApiStoreReducer(newState, action); + expect(newState.data['Article']['1']).toBeDefined(); + expect(newState2.data['Article']['1']).toBeDefined(); + expect(newState2 === newState).toBeTruthy(); + }); + + it('should not update state on second identical patch', () => { + let action = new PatchStoreResourceAction( + { type: 'Article', id: '1', attributes : {title : 'sample title', description : 'test description'} } + ); + let newState = NgrxJsonApiStoreReducer(state, action); + let newState2 = NgrxJsonApiStoreReducer(newState, action); + expect(newState.data['Article']['1']).toBeDefined(); + expect(newState2.data['Article']['1']).toBeDefined(); + expect(newState2 === newState).toBeTruthy(); + }); + + it('should not update state on second partial patch', () => { + let newState = NgrxJsonApiStoreReducer(state, new PatchStoreResourceAction( + { type: 'Article', id: '1', attributes : {title : 'sample title', description : 'sample description'} } + )); + let newState2 = NgrxJsonApiStoreReducer(newState, new PatchStoreResourceAction( + { type: 'Article', id: '1', attributes : {title : 'sample title'} } + )); + expect(newState.data['Article']['1'].resource.attributes.title).toEqual("sample title"); + expect(newState.data['Article']['1'].resource.attributes.description).toEqual("sample description"); + expect(newState2.data['Article']['1'].resource.attributes.title).toEqual("sample title"); + expect(newState2.data['Article']['1'].resource.attributes.description).toEqual("sample description"); + expect(newState2 === newState).toBeTruthy(); + }); }); describe('API_COMMIT_INIT action', () => { diff --git a/src/reducers.ts b/src/reducers.ts index e36f76d..576a576 100644 --- a/src/reducers.ts +++ b/src/reducers.ts @@ -1,3 +1,4 @@ +import * as _ from 'lodash'; import { Action, ActionReducer } from '@ngrx/store'; import { @@ -38,10 +39,10 @@ export const NgrxJsonApiStoreReducer: ActionReducer = // console.log("reduce", state, action); switch (action.type) { - case NgrxJsonApiActionTypes.API_CREATE_INIT: + case NgrxJsonApiActionTypes.API_CREATE_INIT:{ return Object.assign({}, state, { 'isCreating': true }); - - case NgrxJsonApiActionTypes.API_READ_INIT: + } + case NgrxJsonApiActionTypes.API_READ_INIT:{ newState = Object.assign({}, state, { 'isReading': true }); let query = action.payload.query as ResourceQuery; // FIXME: handle queries with no queryId @@ -51,21 +52,21 @@ export const NgrxJsonApiStoreReducer: ActionReducer = }); } return newState; - - case NgrxJsonApiActionTypes.REMOVE_QUERY: + } + case NgrxJsonApiActionTypes.REMOVE_QUERY:{ let queryId = action.payload as string; newState = Object.assign({}, state, { queries: removeQuery( state.queries, queryId), }); return newState; - - case NgrxJsonApiActionTypes.API_UPDATE_INIT: + } + case NgrxJsonApiActionTypes.API_UPDATE_INIT:{ return Object.assign({}, state, { 'isUpdating': true }); - - case NgrxJsonApiActionTypes.API_DELETE_INIT: + } + case NgrxJsonApiActionTypes.API_DELETE_INIT:{ return Object.assign({}, state, { 'isDeleting': true }); - - case NgrxJsonApiActionTypes.API_CREATE_SUCCESS: + } + case NgrxJsonApiActionTypes.API_CREATE_SUCCESS:{ newState = Object.assign({}, state, { data: updateStoreResources( @@ -74,8 +75,8 @@ export const NgrxJsonApiStoreReducer: ActionReducer = { 'isCreating': false } ); return newState; - - case NgrxJsonApiActionTypes.API_READ_SUCCESS: + } + case NgrxJsonApiActionTypes.API_READ_SUCCESS:{ newState = Object.assign({}, state, { data: updateStoreResources( @@ -86,8 +87,8 @@ export const NgrxJsonApiStoreReducer: ActionReducer = { 'isReading': false } ); return newState; - - case NgrxJsonApiActionTypes.API_UPDATE_SUCCESS: + } + case NgrxJsonApiActionTypes.API_UPDATE_SUCCESS:{ newState = Object.assign( {}, state, { @@ -97,42 +98,42 @@ export const NgrxJsonApiStoreReducer: ActionReducer = { 'isUpdating': false } ); return newState; - - case NgrxJsonApiActionTypes.API_DELETE_SUCCESS: + } + case NgrxJsonApiActionTypes.API_DELETE_SUCCESS:{ newState = Object.assign({}, state, { data: deleteStoreResources(state.data, action.payload.query) }, { 'isDeleting': false }); return newState; - - case NgrxJsonApiActionTypes.API_CREATE_FAIL: + } + case NgrxJsonApiActionTypes.API_CREATE_FAIL:{ newState = Object.assign({}, state, { data: updateResourceErrors(state.data, action.payload.query, action.payload.jsonApiData), 'isCreating': false } ); return newState; - - case NgrxJsonApiActionTypes.API_READ_FAIL: + } + case NgrxJsonApiActionTypes.API_READ_FAIL:{ newState = Object.assign({}, state, { queries: updateQueryErrors(state.queries, action.payload.query.queryId, action.payload.jsonApiData), 'isReading': false }); return newState; - - case NgrxJsonApiActionTypes.API_UPDATE_FAIL: + } + case NgrxJsonApiActionTypes.API_UPDATE_FAIL:{ newState = Object.assign({}, state, { data: updateResourceErrors(state.data, action.payload.query, action.payload.jsonApiData), 'isUpdating': false } ); return newState; - - case NgrxJsonApiActionTypes.API_DELETE_FAIL: + } + case NgrxJsonApiActionTypes.API_DELETE_FAIL:{ newState = Object.assign({}, state, { data: updateResourceErrors(state.data, action.payload.query, action.payload.jsonApiData), 'isDeleting': false } ); return newState; - - case NgrxJsonApiActionTypes.QUERY_STORE_SUCCESS: + } + case NgrxJsonApiActionTypes.QUERY_STORE_SUCCESS:{ newState = Object.assign({}, state, { queries: updateQueryResults( state.queries, @@ -140,24 +141,34 @@ export const NgrxJsonApiStoreReducer: ActionReducer = action.payload.jsonApiData), }) return newState; - - case NgrxJsonApiActionTypes.PATCH_STORE_RESOURCE: - newState = Object.assign({}, - state, { - data: updateOrInsertResource( - state.data, action.payload, false, false) - } - ); - return newState; - case NgrxJsonApiActionTypes.POST_STORE_RESOURCE: - newState = Object.assign({}, - state, { - data: updateOrInsertResource( - state.data, action.payload, false, true) - } - ); - return newState; - case NgrxJsonApiActionTypes.DELETE_STORE_RESOURCE: + } + case NgrxJsonApiActionTypes.PATCH_STORE_RESOURCE:{ + let updatedData = updateOrInsertResource(state.data, action.payload, false, false); + if(updatedData !== state.data) { + newState = Object.assign({}, + state, { + data: updatedData + } + ); + return newState; + }else{ + return state; + } + } + case NgrxJsonApiActionTypes.POST_STORE_RESOURCE:{ + let updatedData = updateOrInsertResource(state.data, action.payload, false, true) + if(updatedData !== state.data) { + newState = Object.assign({}, + state, { + data: updatedData + } + ); + return newState; + }else{ + return state; + } + } + case NgrxJsonApiActionTypes.DELETE_STORE_RESOURCE: { newState = Object.assign({}, state, { data: updateResourceState( @@ -165,26 +176,30 @@ export const NgrxJsonApiStoreReducer: ActionReducer = } ); return newState; - case NgrxJsonApiActionTypes.API_COMMIT_INIT: - newState = Object.assign({}, state, { 'isCommitting': true }); + } + case NgrxJsonApiActionTypes.API_COMMIT_INIT: { + newState = Object.assign({}, state, {'isCommitting': true}); return newState; + } case NgrxJsonApiActionTypes.API_COMMIT_SUCCESS: - case NgrxJsonApiActionTypes.API_COMMIT_FAIL: + case NgrxJsonApiActionTypes.API_COMMIT_FAIL: { // apply all the committed or failed changes let actions = action.payload as Array; newState = state; - for(let commitAction of actions){ + for (let commitAction of actions) { newState = NgrxJsonApiStoreReducer(newState, commitAction); } newState = Object.assign({}, newState, {isCommitting: false}); return newState; - case NgrxJsonApiActionTypes.API_ROLLBACK: + } + case NgrxJsonApiActionTypes.API_ROLLBACK:{ newState = Object.assign({}, state, { data: rollbackStoreResources(state.data) } ); return newState; + } default: return state; } diff --git a/src/utils.ts b/src/utils.ts index 318f2b2..bedcf8d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -182,9 +182,7 @@ export const insertStoreResource = (state: NgrxJsonApiStoreResources, loading : false }; } - - return newState; - + return _.isEqual(state, newState) ? state : newState; }; export const updateStoreResource = (state: NgrxJsonApiStoreResources, @@ -224,7 +222,8 @@ export const updateStoreResource = (state: NgrxJsonApiStoreResources, errors : [], loading : false }; - return newState; + + return _.isEqual(newState[resource.id], state[resource.id]) ? state : newState; }; @@ -292,8 +291,6 @@ export const rollbackStoreResources = (state: NgrxJsonApiStoreData): NgrxJsonApi export const updateOrInsertResource = (state: NgrxJsonApiStoreData, resource: Resource, fromServer : boolean, override : boolean): NgrxJsonApiStoreData => { - let newState: NgrxJsonApiStoreData = Object.assign({}, state); - // handle relationships first. // FIXME this is not working, the data section of a relationship contains only , not a complete resource //if (resource.hasOwnProperty('relationships')) { @@ -315,18 +312,27 @@ export const updateOrInsertResource = (state: NgrxJsonApiStoreData, if (_.isUndefined(state[resource.type])) { // we must mutate the main state (ngrxjsonapistoredata) + let newState: NgrxJsonApiStoreData = Object.assign({}, state); newState[resource.type] = {}; newState[resource.type] = insertStoreResource(newState[resource.type], resource, fromServer); return newState; } else if (_.isUndefined(state[resource.type][resource.id]) || override) { - newState[resource.type] = insertStoreResource( - newState[resource.type], resource, fromServer); - return newState; + let updatedTypeState = insertStoreResource(state[resource.type], resource, fromServer); + if(updatedTypeState !== state[resource.type]) { + let newState: NgrxJsonApiStoreData = Object.assign({}, state); + newState[resource.type] = updatedTypeState; + return newState; + } + return state; } else { - newState[resource.type] = updateStoreResource( - newState[resource.type], resource, fromServer); - return newState; + let updatedTypeState = updateStoreResource(state[resource.type], resource, fromServer); + if(updatedTypeState !== state[resource.type]){ + let newState: NgrxJsonApiStoreData = Object.assign({}, state); + newState[resource.type] = updatedTypeState; + return newState; + } + return state; } };