From a288f944dc6a74af3c99f5f1d42c6002159287ea Mon Sep 17 00:00:00 2001
From: Christina Jenks <christina.jenks@ess.eu>
Date: Thu, 12 Oct 2023 13:12:51 +0000
Subject: [PATCH] CE-1954: fix api calls not aborting on unmount or paging
 actions

---
 package-lock.json                             | 14 ++++----
 package.json                                  |  2 +-
 .../AdministerUndeployment.js                 |  7 ++--
 src/components/IOC/IOCTable/IOCStatus.js      |  3 +-
 src/components/common/User/UserIOCList.js     | 10 ++++--
 .../common/User/UserOperationList.js          | 10 ++++--
 .../common/notification/Watchers.js           | 14 +++++---
 .../deployments/DeploymentJobOutput.js        | 11 ++++--
 src/components/records/RecordSearch.js        | 10 ++++--
 .../DeploymentLineChart.js                    |  9 +++--
 .../HostStatistics/HostStatistics.js          | 33 ++++++++++++++---
 .../statistics/IOCStatistics/IOCStatistics.js | 33 ++++++++++++++---
 .../OperationChart/OperationChart.js          |  9 +++--
 src/hooks/Polling.js                          | 20 ++++++++---
 src/stories/views/IOC/IocListView.stories.js  | 27 ++++++++++++++
 src/views/IOC/IOCDetailsContainer.js          |  4 ++-
 src/views/IOC/IOCDetailsView.js               | 35 ++++++++++++-------
 src/views/IOC/IOCListView.js                  | 17 +++++++--
 src/views/host/HostDetailsView.js             | 10 ++++--
 src/views/host/HostListView.js                | 10 ++++--
 src/views/jobs/JobListView.js                 | 11 ++++--
 src/views/records/RecordListView.js           | 10 ++++--
 src/views/statistics/StatisticsView.js        | 21 +++++++----
 23 files changed, 259 insertions(+), 71 deletions(-)
 create mode 100644 src/stories/views/IOC/IocListView.stories.js

diff --git a/package-lock.json b/package-lock.json
index d1f8551b..959f3bab 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,7 +11,7 @@
         "@ahooksjs/use-url-state": "^3.5.0",
         "@emotion/react": "^11.11.1",
         "@emotion/styled": "^11.11.0",
-        "@ess-ics/ce-ui-common": "^0.4.1",
+        "@ess-ics/ce-ui-common": "^0.4.2",
         "@fontsource/roboto": "^4.1.0",
         "@mui/icons-material": "^5.14.1",
         "@mui/material": "^5.14.1",
@@ -2655,9 +2655,9 @@
       }
     },
     "node_modules/@ess-ics/ce-ui-common": {
-      "version": "0.4.1",
-      "resolved": "https://artifactory.esss.lu.se/artifactory/api/npm/ics-npm/@ess-ics/ce-ui-common/-/ce-ui-common-0.4.1.tgz",
-      "integrity": "sha1-xsfA4ukUWTChYXrb0J681o7sKmc=",
+      "version": "0.4.2",
+      "resolved": "https://artifactory.esss.lu.se/artifactory/api/npm/ics-npm/@ess-ics/ce-ui-common/-/ce-ui-common-0.4.2.tgz",
+      "integrity": "sha1-bHkj1S9gtfMc3yF9gn1tSzwPJ6E=",
       "dependencies": {
         "@fontsource/titillium-web": "^4.5.9",
         "@mui/x-data-grid-pro": "^6.5.0",
@@ -41722,9 +41722,9 @@
       "dev": true
     },
     "@ess-ics/ce-ui-common": {
-      "version": "0.4.1",
-      "resolved": "https://artifactory.esss.lu.se/artifactory/api/npm/ics-npm/@ess-ics/ce-ui-common/-/ce-ui-common-0.4.1.tgz",
-      "integrity": "sha1-xsfA4ukUWTChYXrb0J681o7sKmc=",
+      "version": "0.4.2",
+      "resolved": "https://artifactory.esss.lu.se/artifactory/api/npm/ics-npm/@ess-ics/ce-ui-common/-/ce-ui-common-0.4.2.tgz",
+      "integrity": "sha1-bHkj1S9gtfMc3yF9gn1tSzwPJ6E=",
       "requires": {
         "@fontsource/titillium-web": "^4.5.9",
         "@mui/x-data-grid-pro": "^6.5.0",
diff --git a/package.json b/package.json
index 7a41cc6e..3eedaea6 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
     "@ahooksjs/use-url-state": "^3.5.0",
     "@emotion/react": "^11.11.1",
     "@emotion/styled": "^11.11.0",
-    "@ess-ics/ce-ui-common": "^0.4.1",
+    "@ess-ics/ce-ui-common": "^0.4.2",
     "@fontsource/roboto": "^4.1.0",
     "@mui/icons-material": "^5.14.1",
     "@mui/material": "^5.14.1",
diff --git a/src/components/IOC/AdministerUndeployment/AdministerUndeployment.js b/src/components/IOC/AdministerUndeployment/AdministerUndeployment.js
index c6f175f6..4a034dbd 100644
--- a/src/components/IOC/AdministerUndeployment/AdministerUndeployment.js
+++ b/src/components/IOC/AdministerUndeployment/AdministerUndeployment.js
@@ -13,12 +13,15 @@ import {
   Tooltip,
   LinearProgress
 } from "@mui/material";
-import { SimpleAccordion, ConfirmationDialog } from "@ess-ics/ce-ui-common";
+import {
+  SimpleAccordion,
+  ConfirmationDialog,
+  useAPIMethod
+} from "@ess-ics/ce-ui-common";
 import Alert from "@mui/material/Alert";
 import AccessControl from "../../auth/AccessControl";
 import { IocActiveDeployment } from "../../../api/DataTypes";
 import { apiContext } from "../../../api/DeployApi";
-import { useAPIMethod } from "@ess-ics/ce-ui-common/dist/hooks/API";
 
 export default function AdministerUndeployment({ ioc, buttonDisabled }) {
   const navigate = useNavigate();
diff --git a/src/components/IOC/IOCTable/IOCStatus.js b/src/components/IOC/IOCTable/IOCStatus.js
index d67a352b..14e51ff3 100644
--- a/src/components/IOC/IOCTable/IOCStatus.js
+++ b/src/components/IOC/IOCTable/IOCStatus.js
@@ -21,8 +21,7 @@ export const IOCStatus = ({ id, activeDeployment }) => {
     dataReady
   } = useAPIMethod({
     fcn: client.apis.Monitoring.fetchIocStatus,
-    params,
-    call: true
+    params
   });
 
   return (
diff --git a/src/components/common/User/UserIOCList.js b/src/components/common/User/UserIOCList.js
index aac7a9ec..4f5aa493 100644
--- a/src/components/common/User/UserIOCList.js
+++ b/src/components/common/User/UserIOCList.js
@@ -14,7 +14,8 @@ export function UserIocList({ userName }) {
     value: iocs,
     wrapper: getIocs,
     loading,
-    dataReady
+    dataReady,
+    abort
   } = useAPIMethod({
     fcn: client.apis.IOCs.listIocs,
     call: false
@@ -39,11 +40,16 @@ export function UserIocList({ userName }) {
     requestParams.created_by = userName;
 
     getIocs(requestParams);
-  }, [getIocs, pagination, userName]);
+
+    return () => {
+      abort();
+    };
+  }, [getIocs, pagination, userName, abort]);
 
   // Invoked by Table on change to pagination
   const onPage = (params) => {
     setPagination(params);
+    abort();
   };
 
   return (
diff --git a/src/components/common/User/UserOperationList.js b/src/components/common/User/UserOperationList.js
index 75372ca2..016621e9 100644
--- a/src/components/common/User/UserOperationList.js
+++ b/src/components/common/User/UserOperationList.js
@@ -14,7 +14,8 @@ export function UserOperationList({ userName }) {
     value: operations,
     wrapper: getOperations,
     loading,
-    dataReady
+    dataReady,
+    abort
   } = useAPIMethod({
     fcn: client.apis.Deployments.listOperations,
     call: false
@@ -38,11 +39,16 @@ export function UserOperationList({ userName }) {
     requestParams.user = userName;
 
     getOperations(requestParams);
-  }, [getOperations, pagination, userName]);
+
+    return () => {
+      abort();
+    };
+  }, [getOperations, pagination, userName, abort]);
 
   // Invoked by Table on change to pagination
   const onPage = (params) => {
     setPagination(params);
+    abort();
   };
 
   return (
diff --git a/src/components/common/notification/Watchers.js b/src/components/common/notification/Watchers.js
index 209783f3..fd0a8867 100644
--- a/src/components/common/notification/Watchers.js
+++ b/src/components/common/notification/Watchers.js
@@ -29,7 +29,8 @@ export function DeploymentWatcher({
     value: deployment,
     wrapper: getDeployment,
     loading: deploymentLoading,
-    dataReady: deploymentDataReady
+    dataReady: deploymentDataReady,
+    abort: abortGetDeployment
   } = useAPIMethod({
     fcn: client.apis.Deployments.fetchOperation,
     call: false,
@@ -45,7 +46,8 @@ export function DeploymentWatcher({
     value: deploymentJob,
     wrapper: getDeploymentJob,
     loading: jobLoading,
-    dataReady: jobDataReady
+    dataReady: jobDataReady,
+    abort: abortGetDeploymentJob
   } = useAPIMethod({
     fcn: client.apis.Deployments.fetchJobDetails,
     params: deploymentJobParams
@@ -54,13 +56,17 @@ export function DeploymentWatcher({
   useSafePolling(
     getDeployment,
     deploymentLoading || !deploymentDataReady,
-    STATUS_POLL_INTERVAL
+    STATUS_POLL_INTERVAL,
+    true,
+    abortGetDeployment
   );
 
   useSafePolling(
     getDeploymentJob,
     jobLoading || !jobDataReady,
-    STATUS_POLL_INTERVAL
+    STATUS_POLL_INTERVAL,
+    true,
+    abortGetDeploymentJob
   );
 
   const deploymentStatus = useMemo(
diff --git a/src/components/deployments/DeploymentJobOutput.js b/src/components/deployments/DeploymentJobOutput.js
index 6356477e..c34141e3 100644
--- a/src/components/deployments/DeploymentJobOutput.js
+++ b/src/components/deployments/DeploymentJobOutput.js
@@ -26,7 +26,8 @@ export function DeploymentJobOutput({ deploymentJob }) {
     value: log,
     wrapper: getLogById,
     loading: logLoading,
-    dataReady: logDataReady
+    dataReady: logDataReady,
+    abort: abortGetLogById
   } = useAPIMethod({
     fcn: client.apis.Deployments.fetchDeploymentJobLog,
     params
@@ -44,7 +45,13 @@ export function DeploymentJobOutput({ deploymentJob }) {
       finalResultsNeeded.current = !deploymentJob.finished;
     }
   }, [deploymentJob.finished, deploymentJob.id, getLogById]);
-  useSafePolling(getLog, logLoading || !logDataReady, LOG_POLL_INTERVAL);
+  useSafePolling(
+    getLog,
+    logLoading || !logDataReady,
+    LOG_POLL_INTERVAL,
+    true,
+    abortGetLogById
+  );
 
   const dataReady = useCallback(() => {
     return deploymentJob?.started;
diff --git a/src/components/records/RecordSearch.js b/src/components/records/RecordSearch.js
index 2aeba701..39fc9f7b 100644
--- a/src/components/records/RecordSearch.js
+++ b/src/components/records/RecordSearch.js
@@ -22,7 +22,8 @@ export function RecordSearch({ iocName, rowType }) {
     value: records,
     wrapper: getRecords,
     loading,
-    dataReady
+    dataReady,
+    abort
   } = useAPIMethod({
     fcn: client.apis.Records.findAllRecords,
     call: false
@@ -110,7 +111,11 @@ export function RecordSearch({ iocName, rowType }) {
     requestParams.pv_status = recordFilter;
     requestParams.record_name = deserialize(urlState.query);
     getRecords(requestParams);
-  }, [getRecords, recordFilter, urlState.query, pagination]);
+
+    return () => {
+      abort();
+    };
+  }, [getRecords, recordFilter, urlState.query, pagination, abort]);
 
   // Callback for searchbar, called whenever user updates search
   const setSearch = useCallback(
@@ -123,6 +128,7 @@ export function RecordSearch({ iocName, rowType }) {
   // Invoked by Table on change to pagination
   const onPage = (params) => {
     setPagination(params);
+    abort();
   };
 
   return (
diff --git a/src/components/statistics/DeploymentLineChart/DeploymentLineChart.js b/src/components/statistics/DeploymentLineChart/DeploymentLineChart.js
index 809384c9..b22de827 100644
--- a/src/components/statistics/DeploymentLineChart/DeploymentLineChart.js
+++ b/src/components/statistics/DeploymentLineChart/DeploymentLineChart.js
@@ -17,13 +17,18 @@ export default function DeploymentLineChart({
   title,
   chartLabel,
   iocDeployments,
-  getIOCDeployments
+  getIOCDeployments,
+  abortGetIOCDeployments
 }) {
   const theme = useTheme();
 
   useEffect(() => {
     getIOCDeployments();
-  }, [getIOCDeployments]);
+
+    return () => {
+      abortGetIOCDeployments();
+    };
+  }, [getIOCDeployments, abortGetIOCDeployments]);
 
   const GRAPH_THRESHOLD = 0.05;
 
diff --git a/src/components/statistics/HostStatistics/HostStatistics.js b/src/components/statistics/HostStatistics/HostStatistics.js
index 5ffe42dd..6b9302fe 100644
--- a/src/components/statistics/HostStatistics/HostStatistics.js
+++ b/src/components/statistics/HostStatistics/HostStatistics.js
@@ -12,15 +12,27 @@ export function HostStatistics() {
 
   // Do not call on render; when one of these finishes then it and the others
   // will be called again on the next render; this causes an infinite loop.
-  const { value: hostsRegistered, wrapper: getHostsRegistered } = useAPIMethod({
+  const {
+    value: hostsRegistered,
+    wrapper: getHostsRegistered,
+    abort: abortGetHostsRegistered
+  } = useAPIMethod({
     fcn: client.apis.Statistics.calculateStatistics,
     call: false
   });
-  const { value: hostsWithIocs, wrapper: getHostsWithIocs } = useAPIMethod({
+  const {
+    value: hostsWithIocs,
+    wrapper: getHostsWithIocs,
+    abort: abortGetHostsWithIocs
+  } = useAPIMethod({
     fcn: client.apis.Statistics.calculateStatistics,
     call: false
   });
-  const { value: hostsReachable, wrapper: getHostsReachable } = useAPIMethod({
+  const {
+    value: hostsReachable,
+    wrapper: getHostsReachable,
+    abort: abortGetHostsReachable
+  } = useAPIMethod({
     fcn: client.apis.Statistics.calculateStatistics,
     call: false
   });
@@ -29,7 +41,20 @@ export function HostStatistics() {
     getHostsRegistered({ statistics_type: "HOSTS_REGISTERED" });
     getHostsWithIocs({ statistics_type: "HOSTS_WITH_IOCS" });
     getHostsReachable({ statistics_type: "HOSTS_REACHABLE" });
-  }, [getHostsRegistered, getHostsWithIocs, getHostsReachable]);
+
+    return () => {
+      abortGetHostsRegistered();
+      abortGetHostsWithIocs();
+      abortGetHostsReachable();
+    };
+  }, [
+    getHostsRegistered,
+    getHostsWithIocs,
+    getHostsReachable,
+    abortGetHostsRegistered,
+    abortGetHostsWithIocs,
+    abortGetHostsReachable
+  ]);
 
   const hostStats = {
     "Registered IOC-hosts": renderValue(hostsRegistered?.value),
diff --git a/src/components/statistics/IOCStatistics/IOCStatistics.js b/src/components/statistics/IOCStatistics/IOCStatistics.js
index ced1af4c..2da06824 100644
--- a/src/components/statistics/IOCStatistics/IOCStatistics.js
+++ b/src/components/statistics/IOCStatistics/IOCStatistics.js
@@ -13,15 +13,27 @@ export function IOCStatistics() {
 
   // Do not call on render; when one of these finishes then it and the others
   // will be called again on the next render; this causes an infinite loop.
-  const { value: iocsRegistered, wrapper: getIocsRegistered } = useAPIMethod({
+  const {
+    value: iocsRegistered,
+    wrapper: getIocsRegistered,
+    abort: abortGetIocsRegistered
+  } = useAPIMethod({
     fcn: client.apis.Statistics.calculateStatistics,
     call: false
   });
-  const { value: iocsDeployed, wrapper: getIocsDeployed } = useAPIMethod({
+  const {
+    value: iocsDeployed,
+    wrapper: getIocsDeployed,
+    abort: abortGetIocsDeployed
+  } = useAPIMethod({
     fcn: client.apis.Statistics.calculateStatistics,
     call: false
   });
-  const { value: iocsReachable, wrapper: getIocsReachable } = useAPIMethod({
+  const {
+    value: iocsReachable,
+    wrapper: getIocsReachable,
+    abort: abortGetIocsReachable
+  } = useAPIMethod({
     fcn: client.apis.Statistics.calculateStatistics,
     call: false
   });
@@ -30,7 +42,20 @@ export function IOCStatistics() {
     getIocsRegistered({ statistics_type: "IOCS_REGISTERED" });
     getIocsDeployed({ statistics_type: "IOCS_DEPLOYED" });
     getIocsReachable({ statistics_type: "IOCS_RUNNING" });
-  }, [getIocsDeployed, getIocsReachable, getIocsRegistered]);
+
+    return () => {
+      abortGetIocsRegistered();
+      abortGetIocsDeployed();
+      abortGetIocsReachable();
+    };
+  }, [
+    abortGetIocsDeployed,
+    abortGetIocsReachable,
+    abortGetIocsRegistered,
+    getIocsDeployed,
+    getIocsReachable,
+    getIocsRegistered
+  ]);
 
   const iocStats = {
     "Registered IOCs": renderValue(iocsRegistered?.value),
diff --git a/src/components/statistics/OperationChart/OperationChart.js b/src/components/statistics/OperationChart/OperationChart.js
index d6198aa4..8a4236f6 100644
--- a/src/components/statistics/OperationChart/OperationChart.js
+++ b/src/components/statistics/OperationChart/OperationChart.js
@@ -17,13 +17,18 @@ import {
 export default function OperationChart({
   title,
   iocDeployments,
-  getIOCDeployments
+  getIOCDeployments,
+  abortGetIOCDeployments
 }) {
   const theme = useTheme();
 
   useEffect(() => {
     getIOCDeployments();
-  }, [getIOCDeployments]);
+
+    return () => {
+      abortGetIOCDeployments();
+    };
+  }, [getIOCDeployments, abortGetIOCDeployments]);
 
   return iocDeployments ? (
     <>
diff --git a/src/hooks/Polling.js b/src/hooks/Polling.js
index c8a9de5f..bb238475 100644
--- a/src/hooks/Polling.js
+++ b/src/hooks/Polling.js
@@ -1,14 +1,24 @@
 import { useCallback, useEffect, useRef } from "react";
 
-export function useInterval(callback, interval, call = false) {
+const defaultCleanup = () => {};
+
+export function useInterval(
+  callback,
+  interval,
+  call = false,
+  cleanup = defaultCleanup
+) {
   useEffect(() => {
     if (call) callback(); // call the callback right away
     const id = setInterval(callback, interval);
-    return () => clearInterval(id);
-  }, [call, callback, interval]);
+    return () => {
+      clearInterval(id);
+      cleanup();
+    };
+  }, [call, callback, interval, cleanup]);
 }
 
-export function useSafePolling(callback, busy, interval, call = true) {
+export function useSafePolling(callback, busy, interval, call = true, cleanup) {
   const busyRef = useRef(busy);
   busyRef.current = busy;
 
@@ -17,5 +27,5 @@ export function useSafePolling(callback, busy, interval, call = true) {
       callback();
     }
   }, [callback, busyRef]);
-  useInterval(poll, interval, call);
+  useInterval(poll, interval, call, cleanup);
 }
diff --git a/src/stories/views/IOC/IocListView.stories.js b/src/stories/views/IOC/IocListView.stories.js
new file mode 100644
index 00000000..ab112954
--- /dev/null
+++ b/src/stories/views/IOC/IocListView.stories.js
@@ -0,0 +1,27 @@
+import React from "react";
+import { AppHarness } from "../../../mocks/AppHarness";
+import { rest } from "msw";
+import { handlers } from "../../../mocks/handlers";
+import { IOCListView } from "../../../views/IOC/IOCListView";
+
+export default {
+  title: "Views/IOC/IocListView"
+};
+
+const Template = () => (
+  <AppHarness>
+    <IOCListView />
+  </AppHarness>
+);
+
+export const Default = () => <Template />;
+
+export const LoadingAsyncCells = () => <Template />;
+LoadingAsyncCells.parameters = {
+  msw: {
+    handlers: [
+      rest.get("*/iocs/*", (req, res, ctx) => res(ctx.delay("infinite"))),
+      ...handlers
+    ]
+  }
+};
diff --git a/src/views/IOC/IOCDetailsContainer.js b/src/views/IOC/IOCDetailsContainer.js
index 00aee004..239c6fcc 100644
--- a/src/views/IOC/IOCDetailsContainer.js
+++ b/src/views/IOC/IOCDetailsContainer.js
@@ -26,7 +26,8 @@ export function IOCDetailsContainer({ id }) {
     value: ioc,
     wrapper: getIOC,
     loading,
-    error: fetchError
+    error: fetchError,
+    abort: abortGetIOC
   } = useAPIMethod({
     fcn: client.apis.IOCs.getIoc,
     params
@@ -59,6 +60,7 @@ export function IOCDetailsContainer({ id }) {
         <IOCDetailsView
           ioc={ioc}
           getIOC={getIOC}
+          abortGetIOC={abortGetIOC}
           loading={loading}
         />
       ) : (
diff --git a/src/views/IOC/IOCDetailsView.js b/src/views/IOC/IOCDetailsView.js
index 6fe8961a..f8be1d64 100644
--- a/src/views/IOC/IOCDetailsView.js
+++ b/src/views/IOC/IOCDetailsView.js
@@ -27,7 +27,14 @@ import { usePagination } from "../../hooks/pagination";
 import { apiContext } from "../../api/DeployApi";
 
 const IOC_POLL_INTERVAL = 10000;
-export function IOCDetailsView({ ioc, getIOC, loading }) {
+export function IOCDetailsView({ ioc, getIOC, abortGetIOC, loading }) {
+  const { setTitle } = useContext(GlobalAppBarContext);
+  useEffect(() => {
+    if (ioc) {
+      setTitle(applicationTitle(`IOC Details: ${ioc.namingName}`));
+    }
+  }, [ioc, setTitle]);
+
   const [urlState, setUrlState] = useUrlState(
     {
       tab: "Status",
@@ -45,7 +52,8 @@ export function IOCDetailsView({ ioc, getIOC, loading }) {
     value: operations,
     wrapper: getOperations,
     loading: operationsLoading,
-    dataReady: operationsDataReady
+    dataReady: operationsDataReady,
+    abort: abortGetOperations
   } = useAPIMethod({
     fcn: client.apis.Deployments.listOperations,
     call: false
@@ -64,7 +72,8 @@ export function IOCDetailsView({ ioc, getIOC, loading }) {
     value: ongoingCommand,
     wrapper: getOngoingCommand,
     loading: ongoingCommandLoading,
-    dataReady: ongoingCommandDataReady
+    dataReady: ongoingCommandDataReady,
+    abort: abortGetOngoingCommand
   } = useAPIMethod({
     fcn: client.apis.Deployments.listOperations,
     params: ongoingCommandParams,
@@ -116,13 +125,16 @@ export function IOCDetailsView({ ioc, getIOC, loading }) {
   // Invoked by Table on change to pagination
   const onPage = (params) => {
     setJobPagination(params);
+    abortGetOperations();
   };
 
-  useSafePolling(getIOC, loading, IOC_POLL_INTERVAL);
+  useSafePolling(getIOC, loading, IOC_POLL_INTERVAL, true, abortGetIOC);
   useSafePolling(
     getOngoingCommand,
     ongoingCommandLoading || !ongoingCommandDataReady,
-    IOC_POLL_INTERVAL
+    IOC_POLL_INTERVAL,
+    true,
+    abortGetOngoingCommand
   );
 
   useEffect(() => {
@@ -138,7 +150,11 @@ export function IOCDetailsView({ ioc, getIOC, loading }) {
     let requestParams = initRequestParams(jobPagination);
     requestParams.ioc_id = ioc.id;
     getOperations(requestParams);
-  }, [getOperations, jobPagination, ioc]);
+
+    return () => {
+      abortGetOperations();
+    };
+  }, [getOperations, jobPagination, ioc, abortGetOperations]);
 
   const handleClick = () => {
     navigate(-1);
@@ -160,13 +176,6 @@ export function IOCDetailsView({ ioc, getIOC, loading }) {
     [getIOC]
   );
 
-  const { setTitle } = useContext(GlobalAppBarContext);
-  useEffect(() => {
-    if (ioc) {
-      setTitle(applicationTitle(`IOC Details: ${ioc.namingName}`));
-    }
-  }, [ioc, setTitle]);
-
   const statusTab = (
     <Tab
       key="Status"
diff --git a/src/views/IOC/IOCListView.js b/src/views/IOC/IOCListView.js
index 98d2913e..eb64c08e 100644
--- a/src/views/IOC/IOCListView.js
+++ b/src/views/IOC/IOCListView.js
@@ -30,7 +30,8 @@ export function IOCListView() {
     value: iocs,
     wrapper: getIocs,
     loading,
-    dataReady
+    dataReady,
+    abort
   } = useAPIMethod({
     fcn: client.apis.IOCs.listIocs,
     call: false
@@ -118,6 +119,7 @@ export function IOCListView() {
   // Invoked by Table on change to pagination
   const onPage = (params) => {
     setPagination(params);
+    abort();
   };
 
   useEffect(() => {
@@ -129,7 +131,18 @@ export function IOCListView() {
     requestParams.deployment_status = deploymentStatus;
 
     getIocs(requestParams);
-  }, [getIocs, urlPagination, deploymentStatus, urlState.query, pagination]);
+
+    return () => {
+      abort();
+    };
+  }, [
+    getIocs,
+    urlPagination,
+    deploymentStatus,
+    urlState.query,
+    pagination,
+    abort
+  ]);
 
   // Callback for searchbar, called whenever user updates search
   const setSearch = useCallback(
diff --git a/src/views/host/HostDetailsView.js b/src/views/host/HostDetailsView.js
index d82ac468..350f6d60 100644
--- a/src/views/host/HostDetailsView.js
+++ b/src/views/host/HostDetailsView.js
@@ -48,7 +48,8 @@ export function HostDetailsView({ id, host }) {
     value: iocs,
     wrapper: getIocs,
     loading,
-    dataReady
+    dataReady,
+    abort: abortGetIocs
   } = useAPIMethod({
     fcn: client.apis.Hosts.findAssociatedIocsByHostId,
     call: false
@@ -111,6 +112,7 @@ export function HostDetailsView({ id, host }) {
   // Invoked by Table on change to pagination
   const onPage = (params) => {
     setPagination(params);
+    abortGetIocs();
   };
 
   useEffect(() => {
@@ -119,7 +121,11 @@ export function HostDetailsView({ id, host }) {
     requestParams.host_csentry_id = id;
 
     getIocs(requestParams);
-  }, [getIocs, urlPagination, id]);
+
+    return () => {
+      abortGetIocs();
+    };
+  }, [getIocs, urlPagination, id, abortGetIocs]);
 
   function listToString(list) {
     if (list) {
diff --git a/src/views/host/HostListView.js b/src/views/host/HostListView.js
index f4a773d8..06a365e3 100644
--- a/src/views/host/HostListView.js
+++ b/src/views/host/HostListView.js
@@ -34,7 +34,8 @@ export function HostListView() {
   const {
     value: hosts,
     wrapper: getHosts,
-    loading
+    loading,
+    abort
   } = useAPIMethod({
     fcn: client.apis.Hosts.listHosts,
     call: false
@@ -128,7 +129,11 @@ export function HostListView() {
     );
     requestParams.filter = hostFilter;
     getHosts(requestParams);
-  }, [getHosts, hostFilter, urlState.query, pagination]);
+
+    return () => {
+      abort();
+    };
+  }, [getHosts, hostFilter, urlState.query, pagination, abort]);
 
   // Callback for searchbar, called whenever user updates search
   const setSearch = useCallback(
@@ -141,6 +146,7 @@ export function HostListView() {
   // Invoked by Table on change to pagination
   const onPage = (params) => {
     setPagination(params);
+    abort();
   };
 
   const content = (
diff --git a/src/views/jobs/JobListView.js b/src/views/jobs/JobListView.js
index 6c6e0a76..0ac7543d 100644
--- a/src/views/jobs/JobListView.js
+++ b/src/views/jobs/JobListView.js
@@ -22,7 +22,8 @@ export function JobListView() {
     value: operations,
     wrapper: getOperations,
     loading,
-    dataReady
+    dataReady,
+    abort
   } = useAPIMethod({
     fcn: client.apis.Deployments.listOperations,
     call: false
@@ -122,12 +123,17 @@ export function JobListView() {
     }
 
     getOperations(requestParams);
+
+    return () => {
+      abort();
+    };
   }, [
     getOperations,
     urlPagination,
     urlState.query,
     urlState.job_type,
-    deploymentStatus
+    deploymentStatus,
+    abort
   ]);
 
   const setSearch = useCallback(
@@ -140,6 +146,7 @@ export function JobListView() {
   // Invoked by Table on change to pagination
   const onPage = (params) => {
     setPagination(params);
+    abort();
   };
 
   const content = (
diff --git a/src/views/records/RecordListView.js b/src/views/records/RecordListView.js
index ac35ce4a..e0dba8ee 100644
--- a/src/views/records/RecordListView.js
+++ b/src/views/records/RecordListView.js
@@ -35,7 +35,8 @@ export function RecordListView() {
     value: records,
     wrapper: getRecords,
     loading,
-    dataReady
+    dataReady,
+    abort
   } = useAPIMethod({
     fcn: client.apis.Records.findAllRecords,
     call: false
@@ -126,7 +127,11 @@ export function RecordListView() {
     requestParams.pv_status = recordFilter;
     requestParams.record_name = deserialize(urlState.query);
     getRecords(requestParams);
-  }, [getRecords, recordFilter, urlState.query, pagination]);
+
+    return () => {
+      abort();
+    };
+  }, [getRecords, recordFilter, urlState.query, pagination, abort]);
 
   // Callback for searchbar, called whenever user updates search
   const setSearch = useCallback(
@@ -139,6 +144,7 @@ export function RecordListView() {
   // Invoked by Table on change to pagination
   const onPage = (params) => {
     setPagination(params);
+    abort();
   };
 
   let content = (
diff --git a/src/views/statistics/StatisticsView.js b/src/views/statistics/StatisticsView.js
index 414f4731..31d380c3 100644
--- a/src/views/statistics/StatisticsView.js
+++ b/src/views/statistics/StatisticsView.js
@@ -34,16 +34,23 @@ export function StatisticsView() {
 
   const theme = useTheme();
 
-  const { value: iocsOverTime, wrapper: getIocsOverTime } = useAPIMethod({
+  const {
+    value: iocsOverTime,
+    wrapper: getIocsOverTime,
+    abort: abortGetIocsOverTime
+  } = useAPIMethod({
     fcn: client.apis.Statistics.activeIocHistory,
     call: false
   });
 
-  const { value: operationStatistics, wrapper: getOperationStatistics } =
-    useAPIMethod({
-      fcn: client.apis.Statistics.operationHistory,
-      call: false
-    });
+  const {
+    value: operationStatistics,
+    wrapper: getOperationStatistics,
+    abort: abortGetOperationStatistics
+  } = useAPIMethod({
+    fcn: client.apis.Statistics.operationHistory,
+    call: false
+  });
 
   return (
     <StyledRootPaper>
@@ -74,6 +81,7 @@ export function StatisticsView() {
               title="Operations over time"
               iocDeployments={operationStatistics}
               getIOCDeployments={getOperationStatistics}
+              abortGetIOCDeployments={abortGetOperationStatistics}
             />
           </Box>
         </Grid>
@@ -95,6 +103,7 @@ export function StatisticsView() {
               chartLabel="Active IOCs over time"
               iocDeployments={iocsOverTime}
               getIOCDeployments={getIocsOverTime}
+              abortGetIOCDeployments={abortGetIocsOverTime}
             />
           </Box>
         </Grid>
-- 
GitLab