From cd2b798fef38705a8c39769b63641b9e51bfbd02 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mathias=20Maa=C3=9F?= <mathias.maass@uni-leipzig.de>
Date: Fri, 5 Apr 2019 15:53:15 +0200
Subject: [PATCH] Refactor state update patterns

---
 package.json                           |   6 +
 src/state/reducers/annotations.js      |  47 +++---
 src/state/reducers/companionWindows.js |  17 +--
 src/state/reducers/infoResponses.js    |  49 +++----
 src/state/reducers/manifests.js        |  55 +++----
 src/state/reducers/viewers.js          |  17 +--
 src/state/reducers/windows.js          | 194 +++++++------------------
 7 files changed, 132 insertions(+), 253 deletions(-)

diff --git a/package.json b/package.json
index 8d5e632b7..8020d248a 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,12 @@
     "Jack Reed <phillipjreed@gmail.com> (https://www.jack-reed.com)"
   ],
   "repository": "https://github.com/ProjectMirador/mirador",
+  "size-limit": [
+    {
+      "limit": "370 KB",
+      "path": "dist/mirador.min.js"
+    }
+  ],
   "dependencies": {
     "@material-ui/core": "^3.9.1",
     "@material-ui/icons": "^3.0.2",
diff --git a/src/state/reducers/annotations.js b/src/state/reducers/annotations.js
index 1eca687ac..286c6baab 100644
--- a/src/state/reducers/annotations.js
+++ b/src/state/reducers/annotations.js
@@ -1,3 +1,4 @@
+import { set } from './utils';
 import ActionTypes from '../actions/action-types';
 
 /**
@@ -6,37 +7,25 @@ import ActionTypes from '../actions/action-types';
 export const annotationsReducer = (state = {}, action) => {
   switch (action.type) {
     case ActionTypes.REQUEST_ANNOTATION:
-      return {
-        ...state,
-        [action.canvasId]: {
-          [action.annotationId]: {
-            id: action.annotationId,
-            isFetching: true,
-          },
-        },
-      };
+      return set(state, [action.canvasId, action.annotationId], {
+        id: action.annotationId,
+        isFetching: true,
+      });
+
     case ActionTypes.RECEIVE_ANNOTATION:
-      return {
-        ...state,
-        [action.canvasId]: {
-          [action.annotationId]: {
-            id: action.annotationId,
-            isFetching: false,
-            json: action.annotationJson,
-          },
-        },
-      };
+      return set(state, [action.canvasId, action.annotationId], {
+        id: action.annotationId,
+        isFetching: false,
+        json: action.annotationJson,
+      });
+
     case ActionTypes.RECEIVE_ANNOTATION_FAILURE:
-      return {
-        ...state,
-        [action.canvasId]: {
-          [action.annotationId]: {
-            error: action.error,
-            id: action.annotationId,
-            isFetching: false,
-          },
-        },
-      };
+      return set(state, [action.canvasId, action.annotationId], {
+        error: action.error,
+        id: action.annotationId,
+        isFetching: false,
+      });
+
     default: return state;
   }
 };
diff --git a/src/state/reducers/companionWindows.js b/src/state/reducers/companionWindows.js
index 6cd71b959..3c09a441a 100644
--- a/src/state/reducers/companionWindows.js
+++ b/src/state/reducers/companionWindows.js
@@ -1,28 +1,23 @@
-import {
-  removeIn, setIn, updateIn, merge,
-} from 'immutable';
+import { set, update, unset } from './utils';
 import ActionTypes from '../actions/action-types';
 
 /** */
 export function companionWindowsReducer(state = {}, action) {
   switch (action.type) {
     case ActionTypes.ADD_COMPANION_WINDOW:
-      return setIn(state, [action.id], action.payload);
+      return set(state, [action.id], action.payload);
 
     case ActionTypes.ADD_WINDOW:
-      return action.companionWindows.reduce((newState, cw) => {
-        newState[cw.id] = cw; // eslint-disable-line no-param-reassign
-        return newState;
-      }, state);
+      return action.companionWindows.reduce((acc, cw) => set(acc, [cw.id], cw), state);
 
     case ActionTypes.REMOVE_WINDOW:
-      return action.companionWindowIds.reduce((newState, id) => removeIn(newState, [id]), state);
+      return action.companionWindowIds.reduce((acc, id) => unset(acc, [id]), state);
 
     case ActionTypes.UPDATE_COMPANION_WINDOW:
-      return updateIn(state, [action.id], orig => merge(orig, action.payload));
+      return update(state, [action.id], action.payload);
 
     case ActionTypes.REMOVE_COMPANION_WINDOW:
-      return removeIn(state, [action.id]);
+      return unset(state, [action.id]);
 
     default:
       return state;
diff --git a/src/state/reducers/infoResponses.js b/src/state/reducers/infoResponses.js
index 12808e226..4d7bad70d 100644
--- a/src/state/reducers/infoResponses.js
+++ b/src/state/reducers/infoResponses.js
@@ -1,3 +1,4 @@
+import { set, unset } from './utils';
 import ActionTypes from '../actions/action-types';
 
 /**
@@ -6,38 +7,28 @@ import ActionTypes from '../actions/action-types';
 export const infoResponsesReducer = (state = {}, action) => {
   switch (action.type) {
     case ActionTypes.REQUEST_INFO_RESPONSE:
-      return {
-        ...state,
-        [action.infoId]: {
-          id: action.infoId,
-          isFetching: true,
-        },
-      };
+      return set(state, [action.infoId], {
+        id: action.infoId,
+        isFetching: true,
+      });
+
     case ActionTypes.RECEIVE_INFO_RESPONSE:
-      return {
-        ...state,
-        [action.infoId]: {
-          id: action.infoId,
-          isFetching: false,
-          json: action.infoJson,
-        },
-      };
+      return set(state, [action.infoId], {
+        id: action.infoId,
+        isFetching: false,
+        json: action.infoJson,
+      });
+
     case ActionTypes.RECEIVE_INFO_RESPONSE_FAILURE:
-      return {
-        ...state,
-        [action.infoId]: {
-          error: action.error,
-          id: action.infoId,
-          isFetching: false,
-        },
-      };
+      return set(state, [action.infoId], {
+        error: action.error,
+        id: action.infoId,
+        isFetching: false,
+      });
+
     case ActionTypes.REMOVE_INFO_RESPONSE:
-      return Object.keys(state).reduce((object, key) => {
-        if (key !== action.infoId) {
-          object[key] = state[key]; // eslint-disable-line no-param-reassign
-        }
-        return object;
-      }, {});
+      return unset(state, [action.infoId]);
+
     default: return state;
   }
 };
diff --git a/src/state/reducers/manifests.js b/src/state/reducers/manifests.js
index eab3daf40..94da6ae1b 100644
--- a/src/state/reducers/manifests.js
+++ b/src/state/reducers/manifests.js
@@ -1,4 +1,4 @@
-import omit from 'lodash/omit';
+import { update, unset } from './utils';
 import ActionTypes from '../actions/action-types';
 
 /**
@@ -7,42 +7,29 @@ import ActionTypes from '../actions/action-types';
 export const manifestsReducer = (state = {}, action) => {
   switch (action.type) {
     case ActionTypes.REQUEST_MANIFEST:
-      return {
-        [action.manifestId]: {
-          ...state[action.manifestId],
-          ...action.properties,
-          id: action.manifestId,
-        },
-        ...omit(state, action.manifestId),
-      };
+      return update(state, [action.manifestId], {
+        ...action.properties,
+        id: action.manifestId,
+      });
+
     case ActionTypes.RECEIVE_MANIFEST:
-      return {
-        ...state,
-        [action.manifestId]: {
-          ...state[action.manifestId],
-          error: null, // Explicitly set the error to null in case this is a re-fetch
-          id: action.manifestId,
-          isFetching: false,
-          json: action.manifestJson,
-        },
-      };
+      return update(state, [action.manifestId], {
+        error: null, // Explicitly set the error to null in case this is a re-fetch
+        id: action.manifestId,
+        isFetching: false,
+        json: action.manifestJson,
+      });
+
     case ActionTypes.RECEIVE_MANIFEST_FAILURE:
-      return {
-        ...state,
-        [action.manifestId]: {
-          ...state[action.manifestId],
-          error: action.error,
-          id: action.manifestId,
-          isFetching: false,
-        },
-      };
+      return update(state, [action.manifestId], {
+        error: action.error,
+        id: action.manifestId,
+        isFetching: false,
+      });
+
     case ActionTypes.REMOVE_MANIFEST:
-      return Object.keys(state).reduce((object, key) => {
-        if (key !== action.manifestId) {
-          object[key] = state[key]; // eslint-disable-line no-param-reassign
-        }
-        return object;
-      }, {});
+      return unset(state, [action.manifestId]);
+
     default: return state;
   }
 };
diff --git a/src/state/reducers/viewers.js b/src/state/reducers/viewers.js
index e31e42d03..9a85d3ffc 100644
--- a/src/state/reducers/viewers.js
+++ b/src/state/reducers/viewers.js
@@ -1,3 +1,4 @@
+import { set, unset } from './utils';
 import ActionTypes from '../actions/action-types';
 
 /**
@@ -6,19 +7,11 @@ import ActionTypes from '../actions/action-types';
 export const viewersReducer = (state = {}, action) => {
   switch (action.type) {
     case ActionTypes.UPDATE_VIEWPORT:
-      return {
-        ...state,
-        [action.windowId]: {
-          ...action.payload,
-        },
-      };
+      return set(state, [action.windowId], action.payload);
+
     case ActionTypes.REMOVE_WINDOW:
-      return Object.keys(state).reduce((object, key) => {
-        if (key !== action.windowId) {
-          object[key] = state[key]; // eslint-disable-line no-param-reassign
-        }
-        return object;
-      }, {});
+      return unset(state, [action.windowId]);
+
     default:
       return state;
   }
diff --git a/src/state/reducers/windows.js b/src/state/reducers/windows.js
index 76bd83ee1..3b8b01337 100644
--- a/src/state/reducers/windows.js
+++ b/src/state/reducers/windows.js
@@ -1,4 +1,4 @@
-import { remove, updateIn, merge } from 'immutable';
+import { set, update, unset } from './utils';
 import ActionTypes from '../actions/action-types';
 
 /**
@@ -7,83 +7,49 @@ import ActionTypes from '../actions/action-types';
 export const windowsReducer = (state = {}, action) => {
   switch (action.type) {
     case ActionTypes.ADD_WINDOW:
-      return { ...state, [action.window.id]: action.window };
+      return set(state, [action.window.id], action.window);
 
     case ActionTypes.MAXIMIZE_WINDOW:
-      return {
-        ...state,
-        [action.windowId]: {
-          ...state[action.windowId],
-          maximized: true,
-        },
-      };
+      return set(state, [action.windowId, 'maximized'], true);
+
     case ActionTypes.MINIMIZE_WINDOW:
-      return {
-        ...state,
-        [action.windowId]: {
-          ...state[action.windowId],
-          maximized: false,
-        },
-      };
+      return set(state, [action.windowId, 'maximized'], false);
 
     case ActionTypes.UPDATE_WINDOW:
-      return updateIn(state, [action.id], orig => merge(orig, action.payload));
+      return update(state, [action.id], action.payload);
 
     case ActionTypes.REMOVE_WINDOW:
-      return Object.keys(state).reduce((object, key) => {
-        if (key !== action.windowId) {
-          object[key] = state[key]; // eslint-disable-line no-param-reassign
-        }
-        return object;
-      }, {});
+      return unset(state, [action.windowId]);
+
     case ActionTypes.TOGGLE_WINDOW_SIDE_BAR:
-      return {
-        ...state,
-        [action.windowId]: {
-          ...state[action.windowId],
-          sideBarOpen: !state[action.windowId].sideBarOpen,
-        },
-      };
+      return update(state, [action.windowId], props => ({
+        ...props,
+        sideBarOpen: !props.sideBarOpen,
+      }));
+
     case ActionTypes.SET_WINDOW_VIEW_TYPE:
-      return {
-        ...state,
-        [action.windowId]: {
-          ...state[action.windowId],
-          view: action.viewType,
-        },
-      };
+      return set(state, [action.windowId, 'view'], action.viewType);
+
     case ActionTypes.SET_WINDOW_SIDE_BAR_PANEL:
-      return {
-        ...state,
-        [action.windowId]: {
-          ...state[action.windowId],
-          sideBarPanel: (
-            action.panelType
-          ),
-        },
-      };
+      return set(state, [action.windowId, 'sideBarPanel'], action.panelType);
+
     case ActionTypes.UPDATE_WINDOW_POSITION:
-      return {
-        ...state,
-        [action.payload.windowId]: {
-          ...state[action.payload.windowId],
-          x: action.payload.position.x,
-          y: action.payload.position.y,
-        },
-      };
+      return update(state, [action.payload.windowId], {
+        x: action.payload.position.x,
+        y: action.payload.position.y,
+      });
+
     case ActionTypes.SET_WINDOW_SIZE:
-      return {
-        ...state,
-        [action.payload.windowId]: {
-          ...state[action.payload.windowId],
-          height: action.payload.size.height,
-          width: action.payload.size.width,
-          x: action.payload.size.x,
-          y: action.payload.size.y,
-        },
-      };
+      return update(state, [action.payload.windowId], {
+        height: action.payload.size.height,
+        width: action.payload.size.width,
+        x: action.payload.size.x,
+        y: action.payload.size.y,
+      });
+
     case ActionTypes.SET_CANVAS:
-      return setCanvasIndex(state, action.windowId, currentIndex => action.canvasIndex);
+      return set(state, [action.windowId, 'canvasIndex'], action.canvasIndex);
+
     case ActionTypes.ADD_COMPANION_WINDOW:
       if (action.payload.position === 'left') {
         const { companionWindowIds } = state[action.windowId];
@@ -91,66 +57,38 @@ export const windowsReducer = (state = {}, action) => {
         const newCompanionWindowIds = companionWindowIds
           .filter(id => companionWindows[id].position !== action.payload.position);
 
-        return {
-          ...state,
-          [action.windowId]: {
-            ...state[action.windowId],
-            companionAreaOpen: true,
-            companionWindowIds: newCompanionWindowIds.concat([action.id]),
-            sideBarPanel: action.payload.content,
-          },
-        };
+        return update(state, [action.windowId], {
+          companionAreaOpen: true,
+          companionWindowIds: newCompanionWindowIds.concat([action.id]),
+          sideBarPanel: action.payload.content,
+        });
       }
+      return update(state, [action.windowId], props => ({
+        ...props,
+        companionWindowIds: props.companionWindowIds.concat([action.id]),
+      }));
 
-      return {
-        ...state,
-        [action.windowId]: {
-          ...state[action.windowId],
-          companionWindowIds: state[action.windowId].companionWindowIds.concat([action.id]),
-        },
-      };
     case ActionTypes.REMOVE_COMPANION_WINDOW:
-      return {
-        ...state,
-        [action.windowId]: {
-          ...state[action.windowId],
-          companionWindowIds: state[action.windowId]
-            .companionWindowIds.filter(id => id !== action.id),
-        },
-      };
+      return update(state, [action.windowId], props => ({
+        ...props,
+        companionWindowIds: props.companionWindowIds.filter(id => id !== action.id),
+      }));
+
     case ActionTypes.SELECT_ANNOTATION:
-      return {
-        ...state,
-        [action.windowId]: {
-          ...state[action.windowId],
-          selectedAnnotations: {
-            ...state[action.windowId].selectedAnnotations,
-            [action.canvasId]: [
-              ...((state[action.windowId].selectedAnnotations || {})[action.canvasId] || []),
-              action.annotationId,
-            ],
-          },
-        },
-      };
+      return update(state, [action.windowId, 'selectedAnnotations', action.canvasId],
+        arr => [...(arr || []), action.annotationId]);
+
     case ActionTypes.DESELECT_ANNOTATION: {
       const selectedAnnotations = updatedSelectedAnnotations(state, action);
-
-      return {
-        ...state,
-        [action.windowId]: {
-          ...state[action.windowId],
-          selectedAnnotations,
-        },
-      };
+      return update(state, [action.windowId], { selectedAnnotations });
     }
+
     case ActionTypes.TOGGLE_ANNOTATION_DISPLAY:
-      return {
-        ...state,
-        [action.windowId]: {
-          ...state[action.windowId],
-          displayAllAnnotations: !state[action.windowId].displayAllAnnotations,
-        },
-      };
+      return update(state, [action.windowId], props => ({
+        ...props,
+        displayAllAnnotations: !props.displayAllAnnotations,
+      }));
+
     default:
       return state;
   }
@@ -172,25 +110,5 @@ function updatedSelectedAnnotations(state, action) {
     };
   }
 
-  return remove(state[action.windowId].selectedAnnotations, action.canvasId);
-}
-
-/**
- * @param {Object} state
- * @param {String} windowId
- * @param {Function} getIndex - gets curent canvas index passed and should return new index
- */
-function setCanvasIndex(state, windowId, getIndex) {
-  return Object.values(state).reduce((object, window) => {
-    if (window.id === windowId) {
-      return {
-        ...object,
-        [window.id]: {
-          ...window,
-          canvasIndex: getIndex(window.canvasIndex),
-        },
-      };
-    }
-    return { ...object, [window.id]: window };
-  }, {});
+  return unset(state[action.windowId].selectedAnnotations, [action.canvasId]);
 }
-- 
GitLab