From 2013e455b71e4b6c2d9a3d668ebb446968ba98c0 Mon Sep 17 00:00:00 2001 From: Julia Wu Date: Wed, 21 Jan 2026 16:45:53 -0800 Subject: [PATCH 1/8] add abort controllers to map --- .../stat_var_hierarchy/stat_var_hierarchy.tsx | 70 ++++++++++++++----- static/js/tools/map/fetcher/all_stat.ts | 16 ++++- static/js/tools/map/fetcher/default_stat.ts | 19 ++++- static/js/utils/data_fetch_utils.ts | 41 +++++++++-- 4 files changed, 117 insertions(+), 29 deletions(-) diff --git a/static/js/stat_var_hierarchy/stat_var_hierarchy.tsx b/static/js/stat_var_hierarchy/stat_var_hierarchy.tsx index 710b0d273a..beaf53d684 100644 --- a/static/js/stat_var_hierarchy/stat_var_hierarchy.tsx +++ b/static/js/stat_var_hierarchy/stat_var_hierarchy.tsx @@ -100,6 +100,9 @@ export class StatVarHierarchy extends React.Component< StatVarHierarchyPropType, StatVarHierarchyStateType > { + // Abort controller to cancel any in-flight requests on re-render + private _abortController: AbortController; + constructor(props: StatVarHierarchyPropType) { super(props); this.state = { @@ -121,6 +124,13 @@ export class StatVarHierarchy extends React.Component< this.fetchData(); } + componentWillUnmount(): void { + if (this._abortController) { + // Abort the any requests generated by the component we are unmounting + this._abortController.abort(); + } + } + componentDidUpdate(prevProps: StatVarHierarchyPropType): void { if (this.state.searchSelectionCleared) { this.setState({ searchSelectionCleared: false }); @@ -257,6 +267,13 @@ export class StatVarHierarchy extends React.Component< } private async fetchData(): Promise { + if (this._abortController) { + // Abort the previous request + this._abortController.abort(); + } + this._abortController = new AbortController(); + const signal = this._abortController.signal; + loadSpinner(SV_HIERARCHY_SECTION_ID); const entityList = this.props.entities.map((entity) => entity.dcid); const variableGroupInfoPromises: Promise[] = @@ -267,11 +284,15 @@ export class StatVarHierarchy extends React.Component< ? [statVarHierarchyConfigNode.dataSourceDcid] : []; return axios - .post("/api/variable-group/info", { - dcid: statVarHierarchyConfigNode.dcid, - entities: [...entityList, ...dataSourceEntities], - numEntitiesExistence: this.props.numEntitiesExistence, - }) + .post( + "/api/variable-group/info", + { + dcid: statVarHierarchyConfigNode.dcid, + entities: [...entityList, ...dataSourceEntities], + numEntitiesExistence: this.props.numEntitiesExistence, + }, + { signal } + ) .then((resp) => { return resp.data; }); @@ -283,7 +304,7 @@ export class StatVarHierarchy extends React.Component< if (this.state.svPath && sv in this.state.svPath) { svPath[sv] = this.state.svPath[sv]; } else { - statVarPathPromises.push(this.getPath(sv)); + statVarPathPromises.push(this.getPath(sv, signal)); } } } @@ -329,8 +350,12 @@ export class StatVarHierarchy extends React.Component< rootSVGs, svPath, }); - } catch { + } catch (error) { removeSpinner(SV_HIERARCHY_SECTION_ID); + // Ignore request cancellation errors + if (axios.isCancel(error) || error.name === "AbortError") { + return; + } this.setState({ errorMessage: "Error retrieving stat var group root nodes", }); @@ -338,17 +363,24 @@ export class StatVarHierarchy extends React.Component< } private onSearchSelectionChange(selection: string): void { - this.getPath(selection).then((path) => { - const searchSelectionCleared = - !_.isEmpty(this.state.focusPath) && _.isEmpty(path); - this.setState({ - focus: selection, - focusPath: path, - searchSelectionCleared, - expandedPath: searchSelectionCleared ? this.state.focusPath : [], + this.getPath(selection, this._abortController?.signal) + .then((path) => { + const searchSelectionCleared = + !_.isEmpty(this.state.focusPath) && _.isEmpty(path); + this.setState({ + focus: selection, + focusPath: path, + searchSelectionCleared, + expandedPath: searchSelectionCleared ? this.state.focusPath : [], + }); + this.togglePath(selection, path, /*isSearchSelection=*/ true); + }) + .catch((error) => { + // Ignore abort errors + if (!axios.isCancel(error) && error.name !== "AbortError") { + console.error(error); + } }); - this.togglePath(selection, path, /*isSearchSelection=*/ true); - }); } // Add or remove a stat var and its path from the state. @@ -387,12 +419,12 @@ export class StatVarHierarchy extends React.Component< } // Get the path of a stat var from the hierarchy. - private getPath(sv: string): Promise { + private getPath(sv: string, signal?: AbortSignal): Promise { if (sv == "") { return Promise.resolve([]); } return axios - .get(`/api/variable/path?dcid=${encodeURIComponent(sv)}`) + .get(`/api/variable/path?dcid=${encodeURIComponent(sv)}`, { signal }) .then((resp) => { // This is to make jest test working, should find a better way to let // mock return new object each time. diff --git a/static/js/tools/map/fetcher/all_stat.ts b/static/js/tools/map/fetcher/all_stat.ts index 99d27944c0..be9a457805 100644 --- a/static/js/tools/map/fetcher/all_stat.ts +++ b/static/js/tools/map/fetcher/all_stat.ts @@ -44,6 +44,10 @@ export function useFetchAllStat(dispatch: Dispatch): void { if (!contextOk) { return; } + // Use AbortController to cancel the request if the component unmounts. + const abortController = new AbortController(); + const signal = abortController.signal; + const action: ChartStoreAction = { type: ChartDataType.ALL_STAT, context: { @@ -72,6 +76,7 @@ export function useFetchAllStat(dispatch: Dispatch): void { }, paramsSerializer: stringifyFn, headers: WEBSITE_SURFACE_HEADER, + signal, }) .then((resp) => { if (_.isEmpty(resp.data.data[statVar.value.dcid])) { @@ -85,10 +90,19 @@ export function useFetchAllStat(dispatch: Dispatch): void { console.log(`[Map Fetch] all stat for date: ${date}`); dispatch(action); }) - .catch(() => { + .catch((error) => { + // Ignore abort errors + if (axios.isCancel(error) || error.name === "AbortError") { + return; + } action.error = "error fetching all stat data"; dispatch(action); }); + + return () => { + // Abort the request if the component unmounts. + abortController.abort(); + }; }, [ placeInfo.value.enclosingPlace.dcid, placeInfo.value.enclosedPlaceType, diff --git a/static/js/tools/map/fetcher/default_stat.ts b/static/js/tools/map/fetcher/default_stat.ts index 46942805e1..b525fe5fb6 100644 --- a/static/js/tools/map/fetcher/default_stat.ts +++ b/static/js/tools/map/fetcher/default_stat.ts @@ -18,6 +18,7 @@ * Fetch the default (best available) stat data */ +import axios from "axios"; import _ from "lodash"; import { Dispatch, useContext, useEffect } from "react"; @@ -40,6 +41,9 @@ export function useFetchDefaultStat( if (!contextOk) { return; } + // Use AbortController to cancel the request if the component unmounts. + const abortController = new AbortController(); + const signal = abortController.signal; const action: ChartStoreAction = { type: ChartDataType.DEFAULT_STAT, @@ -67,7 +71,9 @@ export function useFetchDefaultStat( date, null, // alignedVariables null, // facetIds - WEBSITE_SURFACE + WEBSITE_SURFACE, + undefined, // facetSelector + signal ) .then((resp) => { if (_.isEmpty(resp.data[statVar.value.dcid])) { @@ -81,10 +87,19 @@ export function useFetchDefaultStat( console.log(`[Map Fetch] default stat for date: ${date}`); dispatch(action); }) - .catch(() => { + .catch((error) => { + // Ignore abort errors + if (axios.isCancel(error) || error.name === "AbortError") { + return; + } action.error = "error fetching default stat data"; dispatch(action); }); + + return () => { + // Abort the request if the component unmounts. + abortController.abort(); + }; }, [ dateCtx.value, placeInfo.value.enclosingPlace.dcid, diff --git a/static/js/utils/data_fetch_utils.ts b/static/js/utils/data_fetch_utils.ts index 4d975698c5..eee75213fd 100644 --- a/static/js/utils/data_fetch_utils.ts +++ b/static/js/utils/data_fetch_utils.ts @@ -163,12 +163,19 @@ async function selectFacet( entities: string[], variables: string[], facetSelector?: FacetSelectionCriteria, - surface?: string + surface?: string, + signal?: AbortSignal ): Promise { if (!facetSelector) { return []; } - const facetsResponse = await getFacets(apiRoot, entities, variables, surface); + const facetsResponse = await getFacets( + apiRoot, + entities, + variables, + surface, + signal + ); for (const svDcid of Object.keys(facetsResponse)) { const matchingFacets = findMatchingFacets( facetsResponse[svDcid], @@ -206,11 +213,19 @@ export function getPoint( alignedVariables?: string[][], facetSelector?: FacetSelectionCriteria, facetIds?: string[], - surface?: string + surface?: string, + signal?: AbortSignal ): Promise { const facetPromise = !_.isEmpty(facetIds) ? Promise.resolve(facetIds) - : selectFacet(apiRoot, entities, variables, facetSelector, surface); + : selectFacet( + apiRoot, + entities, + variables, + facetSelector, + surface, + signal + ); return facetPromise.then((resolvedFacetIds) => { const params: Record = { date, entities, variables }; @@ -225,6 +240,7 @@ export function getPoint( params, paramsSerializer: stringifyFn, headers: getSurfaceHeader(surface), + signal, }) .then((resp) => { return getProcessedPointResponse(resp.data, alignedVariables); @@ -256,11 +272,19 @@ export function getPointWithin( alignedVariables?: string[][], facetIds?: string[], surface?: string, - facetSelector?: FacetSelectionCriteria + facetSelector?: FacetSelectionCriteria, + signal?: AbortSignal ): Promise { const facetPromise = !_.isEmpty(facetIds) ? Promise.resolve(facetIds) - : selectFacet(apiRoot, [parentEntity], variables, facetSelector, surface); + : selectFacet( + apiRoot, + [parentEntity], + variables, + facetSelector, + surface, + signal + ); return facetPromise.then((resolvedFacetIds) => { const params = { childType, date, parentEntity, variables }; @@ -275,6 +299,7 @@ export function getPointWithin( params, paramsSerializer: stringifyFn, headers: getSurfaceHeader(surface), + signal, }) .then((resp) => { return getProcessedPointResponse(resp.data, alignedVariables); @@ -419,13 +444,15 @@ export function getFacets( apiRoot: string, entities: string[], variables: string[], - surface?: string + surface?: string, + signal?: AbortSignal ): Promise { return axios .get(`${apiRoot || ""}/api/facets`, { params: { entities, variables }, paramsSerializer: stringifyFn, headers: getSurfaceHeader(surface), + signal, }) .then((resp) => { const respData = resp.data; From cf1878993731110c8a9305be87abd0eae0e4e8c6 Mon Sep 17 00:00:00 2001 From: Julia Wu Date: Wed, 21 Jan 2026 16:49:25 -0800 Subject: [PATCH 2/8] also add abort controller to stat_var_group_node --- .../stat_var_group_node.tsx | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/static/js/stat_var_hierarchy/stat_var_group_node.tsx b/static/js/stat_var_hierarchy/stat_var_group_node.tsx index 49ae941885..ccef900079 100644 --- a/static/js/stat_var_hierarchy/stat_var_group_node.tsx +++ b/static/js/stat_var_hierarchy/stat_var_group_node.tsx @@ -100,6 +100,8 @@ export class StatVarGroupNode extends React.Component< context: ContextType; // the list of entities for which data fetch has begun, but not finished. dataFetchingEntities: NamedNode[]; + // Abort controller to cancel any in-flight requests on re-render + private _abortController: AbortController; constructor(props: StatVarGroupNodePropType) { super(props); @@ -130,6 +132,13 @@ export class StatVarGroupNode extends React.Component< } } + componentWillUnmount(): void { + if (this._abortController) { + // Cancel any existing requests on unmount + this._abortController.abort(); + } + } + componentDidUpdate(prevProps: StatVarGroupNodePropType): void { const newSelectionCount = this.getSelectionCount(); const newState = { ...this.state }; @@ -260,6 +269,13 @@ export class StatVarGroupNode extends React.Component< } private fetchData(): void { + if (this._abortController) { + // Cancel any existing requests + this._abortController.abort(); + } + this._abortController = new AbortController(); + const signal = this._abortController.signal; + const entityList = this.props.entities; this.dataFetchingEntities = this.props.entities; let numEntitiesExistence = this.props.numEntitiesExistence; @@ -272,11 +288,15 @@ export class StatVarGroupNode extends React.Component< numEntitiesExistence = entityDcids.length; } axios - .post("/api/variable-group/info", { - dcid: this.props.data.id, - entities: entityDcids, - numEntitiesExistence, - }) + .post( + "/api/variable-group/info", + { + dcid: this.props.data.id, + entities: entityDcids, + numEntitiesExistence, + }, + { signal } + ) .then((resp) => { const data = resp.data; const childSV: StatVarInfo[] = data["childStatVars"] || []; @@ -290,7 +310,11 @@ export class StatVarGroupNode extends React.Component< }); } }) - .catch(() => { + .catch((error) => { + // Ignore abort errors + if (axios.isCancel(error) || error.name === "AbortError") { + return; + } this.dataFetchingEntities = null; if (_.isEqual(entityList, this.props.entities)) { this.setState({ From facf9c09413b8fcf5126b5b8fea0bc433eea1c3f Mon Sep 17 00:00:00 2001 From: Julia Wu Date: Wed, 21 Jan 2026 16:53:59 -0800 Subject: [PATCH 3/8] Add more abort controllers --- static/js/tools/map/fetcher/all_dates.ts | 16 ++++++++++- static/js/tools/map/fetcher/border_geojson.ts | 27 +++++++++++++++---- .../map/fetcher/breadcrumb_denom_stat.ts | 19 +++++++++++-- .../js/tools/map/fetcher/breadcrumb_stat.ts | 16 ++++++++++- static/js/tools/map/fetcher/denom_stat.ts | 19 +++++++++++-- .../tools/map/fetcher/european_countries.ts | 21 ++++++++++++--- static/js/tools/map/fetcher/geojson.ts | 16 ++++++++++- .../tools/map/fetcher/map_point_coordinate.ts | 16 ++++++++++- static/js/tools/map/fetcher/map_point_stat.ts | 20 ++++++++++++-- .../js/tools/map/fetcher/stat_var_summary.ts | 16 ++++++++++- static/js/utils/data_fetch_utils.ts | 10 ++++--- 11 files changed, 173 insertions(+), 23 deletions(-) diff --git a/static/js/tools/map/fetcher/all_dates.ts b/static/js/tools/map/fetcher/all_dates.ts index eb7fc60668..b8241b7d7a 100644 --- a/static/js/tools/map/fetcher/all_dates.ts +++ b/static/js/tools/map/fetcher/all_dates.ts @@ -37,6 +37,10 @@ export function useFetchAllDates(dispatch: Dispatch): void { if (!contextOk) { return; } + // Use AbortController to cancel the request if the component unmounts + const abortController = new AbortController(); + const signal = abortController.signal; + const action: ChartStoreAction = { type: ChartDataType.ALL_DATES, error: null, @@ -60,6 +64,7 @@ export function useFetchAllDates(dispatch: Dispatch): void { childType: placeInfo.value.enclosedPlaceType, variable: statVar.value.dcid, }, + signal, }) .then((resp) => { const data = resp.data as ObservationDatesResponse; @@ -77,10 +82,19 @@ export function useFetchAllDates(dispatch: Dispatch): void { console.log("[Map Fetch] all dates"); dispatch(action); }) - .catch(() => { + .catch((error) => { + // Ignore abort errors + if (axios.isCancel(error) || error.name === "AbortError") { + return; + } action.error = "error fetching all the dates"; dispatch(action); }); + + return () => { + // Cancel the request if the component unmounts + abortController.abort(); + }; }, [ display.value.showTimeSlider, placeInfo.value.enclosingPlace.dcid, diff --git a/static/js/tools/map/fetcher/border_geojson.ts b/static/js/tools/map/fetcher/border_geojson.ts index e893281e33..ff0d45069b 100644 --- a/static/js/tools/map/fetcher/border_geojson.ts +++ b/static/js/tools/map/fetcher/border_geojson.ts @@ -45,6 +45,10 @@ export function useFetchBorderGeoJson( if (!contextOk || !shouldShowBorder(placeInfo.value.enclosedPlaceType)) { return; } + // Use AbortController to cancel the request if the component unmounts + const abortController = new AbortController(); + const signal = abortController.signal; + const action: ChartStoreAction = { type: ChartDataType.BORDER_GEO_JSON, error: null, @@ -59,10 +63,14 @@ export function useFetchBorderGeoJson( }, }; axios - .post("/api/choropleth/node-geojson", { - nodes: [placeInfo.value.enclosingPlace.dcid], - geoJsonProp: BORDER_GEOJSON_PROPERTY, - }) + .post( + "/api/choropleth/node-geojson", + { + nodes: [placeInfo.value.enclosingPlace.dcid], + geoJsonProp: BORDER_GEOJSON_PROPERTY, + }, + { signal } + ) .then((resp) => { if (_.isEmpty(resp.data)) { action.error = "error fetching border geo json data"; @@ -71,10 +79,19 @@ export function useFetchBorderGeoJson( } dispatch(action); }) - .catch(() => { + .catch((error) => { + // Ignore abort errors + if (axios.isCancel(error) || error.name === "AbortError") { + return; + } action.error = "error fetching border geo json data"; dispatch(action); }); + + return () => { + // Cancel request if component unmounts + abortController.abort(); + }; }, [ placeInfo.value.enclosingPlace, placeInfo.value.enclosedPlaceType, diff --git a/static/js/tools/map/fetcher/breadcrumb_denom_stat.ts b/static/js/tools/map/fetcher/breadcrumb_denom_stat.ts index f3dc31d66a..4ba461dede 100644 --- a/static/js/tools/map/fetcher/breadcrumb_denom_stat.ts +++ b/static/js/tools/map/fetcher/breadcrumb_denom_stat.ts @@ -18,6 +18,7 @@ * Fetch the breadcrumb (parent places) denominator stat data */ +import axios from "axios"; import _ from "lodash"; import { Dispatch, useContext, useEffect } from "react"; @@ -43,6 +44,10 @@ export function useFetchBreadcrumbDenomStat( const placeDcids = placeInfo.value.parentPlaces.map((x) => x.dcid); placeDcids.push(placeInfo.value.selectedPlace.dcid); + // Use AbortController to cancel the request if the component unmounts + const abortController = new AbortController(); + const signal = abortController.signal; + const action: ChartStoreAction = { type: ChartDataType.BREADCRUMB_DENOM_STAT, context: { @@ -67,7 +72,8 @@ export function useFetchBreadcrumbDenomStat( [statVar.value.denom], null, // facetIds null, // highlightFacet - WEBSITE_SURFACE + WEBSITE_SURFACE, + signal ) .then((resp) => { if (_.isEmpty(resp.data[statVar.value.denom])) { @@ -81,10 +87,19 @@ export function useFetchBreadcrumbDenomStat( console.log("[Map Fetch] breadcrumb denom stat"); dispatch(action); }) - .catch(() => { + .catch((error) => { + // Ignore abort errors + if (axios.isCancel(error) || error.name === "AbortError") { + return; + } action.error = "error fetching breadcrumb denom stat data"; dispatch(action); }); + + return () => { + // Cancel request if component unmounts + abortController.abort(); + }; }, [ placeInfo.value.selectedPlace.dcid, placeInfo.value.parentPlaces, diff --git a/static/js/tools/map/fetcher/breadcrumb_stat.ts b/static/js/tools/map/fetcher/breadcrumb_stat.ts index 7b33b64ba4..9b7ad00674 100644 --- a/static/js/tools/map/fetcher/breadcrumb_stat.ts +++ b/static/js/tools/map/fetcher/breadcrumb_stat.ts @@ -47,6 +47,10 @@ export function useFetchBreadcrumbStat( const placeDcids = placeInfo.value.parentPlaces.map((x) => x.dcid); placeDcids.push(placeInfo.value.selectedPlace.dcid); + // Use AbortController to cancel the request if the component unmounts + const abortController = new AbortController(); + const signal = abortController.signal; + const action: ChartStoreAction = { type: ChartDataType.BREADCRUMB_STAT, context: { @@ -75,6 +79,7 @@ export function useFetchBreadcrumbStat( }, paramsSerializer: stringifyFn, headers: WEBSITE_SURFACE_HEADER, + signal, }) .then((resp) => { if (_.isEmpty(resp.data.data[statVar.value.dcid])) { @@ -88,10 +93,19 @@ export function useFetchBreadcrumbStat( console.log(`[Map Fetch] breadcrumb stat for date: ${date}`); dispatch(action); }) - .catch(() => { + .catch((error) => { + // Ignore abort errors + if (axios.isCancel(error) || error.name === "AbortError") { + return; + } action.error = "error fetching breadcrumb stat data"; dispatch(action); }); + + return () => { + // Cancel request if component unmounts + abortController.abort(); + }; }, [ placeInfo.value.selectedPlace.dcid, placeInfo.value.parentPlaces, diff --git a/static/js/tools/map/fetcher/denom_stat.ts b/static/js/tools/map/fetcher/denom_stat.ts index ce39f099a8..0e98e94e6e 100644 --- a/static/js/tools/map/fetcher/denom_stat.ts +++ b/static/js/tools/map/fetcher/denom_stat.ts @@ -18,6 +18,7 @@ * Fetch the stat data for denominator stat var */ +import axios from "axios"; import _ from "lodash"; import { Dispatch, useContext, useEffect } from "react"; @@ -37,6 +38,10 @@ export function useFetchDenomStat(dispatch: Dispatch): void { if (!contextOk) { return; } + // Use AbortController to cancel the request if the component unmounts + const abortController = new AbortController(); + const signal = abortController.signal; + const action: ChartStoreAction = { type: ChartDataType.DENOM_STAT, error: null, @@ -59,7 +64,8 @@ export function useFetchDenomStat(dispatch: Dispatch): void { placeInfo.value.enclosedPlaceType, [statVar.value.denom], null, // facetIds - WEBSITE_SURFACE + WEBSITE_SURFACE, + signal ) .then((resp) => { if (_.isEmpty(resp.data[statVar.value.denom])) { @@ -73,10 +79,19 @@ export function useFetchDenomStat(dispatch: Dispatch): void { console.log("[Map Fetch] denom stat"); dispatch(action); }) - .catch(() => { + .catch((error) => { + // Ignore abort errors + if (axios.isCancel(error) || error.name === "AbortError") { + return; + } action.error = "error fetching denom stat data"; dispatch(action); }); + + return () => { + // Cancel request if component unmounts + abortController.abort(); + }; }, [ placeInfo.value.enclosingPlace.dcid, placeInfo.value.enclosedPlaceType, diff --git a/static/js/tools/map/fetcher/european_countries.ts b/static/js/tools/map/fetcher/european_countries.ts index 927e3efc02..e31c4c0021 100644 --- a/static/js/tools/map/fetcher/european_countries.ts +++ b/static/js/tools/map/fetcher/european_countries.ts @@ -18,6 +18,7 @@ * Fetch european countries. */ +import axios from "axios"; import { useEffect, useState } from "react"; import { EUROPE_NAMED_TYPED_PLACE } from "../../../shared/constants"; @@ -27,12 +28,24 @@ import { getEnclosedPlacesPromise } from "../../../utils/place_utils"; export function useFetchEuropeanCountries(): Array { const [data, setData] = useState>(); useEffect(() => { - getEnclosedPlacesPromise(EUROPE_NAMED_TYPED_PLACE.dcid, "Country").then( - (resp: Array) => { + const abortController = new AbortController(); + const signal = abortController.signal; + + getEnclosedPlacesPromise(EUROPE_NAMED_TYPED_PLACE.dcid, "Country", signal) + .then((resp: Array) => { setData(resp); console.log("[Map Fetch] european countries"); - } - ); + }) + .catch((error) => { + if (axios.isCancel(error) || error.name === "AbortError") { + return; + } + console.error("Error fetching European countries:", error); + }); + + return () => { + abortController.abort(); + }; }, []); return data; } diff --git a/static/js/tools/map/fetcher/geojson.ts b/static/js/tools/map/fetcher/geojson.ts index 784b721c1c..b4243547e3 100644 --- a/static/js/tools/map/fetcher/geojson.ts +++ b/static/js/tools/map/fetcher/geojson.ts @@ -34,6 +34,10 @@ export function useFetchGeoJson(dispatch: Dispatch): void { if (!contextOk) { return; } + // Use AbortController to cancel the request if the component unmounts + const abortController = new AbortController(); + const signal = abortController.signal; + const action: ChartStoreAction = { type: ChartDataType.GEO_JSON, error: null, @@ -53,6 +57,7 @@ export function useFetchGeoJson(dispatch: Dispatch): void { placeDcid: placeInfo.value.enclosingPlace.dcid, placeType: placeInfo.value.enclosedPlaceType, }, + signal, }) .then((resp) => { if (_.isEmpty(resp.data)) { @@ -63,10 +68,19 @@ export function useFetchGeoJson(dispatch: Dispatch): void { console.log("[Map Fetch] geojson"); dispatch(action); }) - .catch(() => { + .catch((error) => { + // Ignore abort errors + if (axios.isCancel(error) || error.name === "AbortError") { + return; + } action.error = "error fetching geo json data"; dispatch(action); }); + + return () => { + // Cancel request if component unmounts + abortController.abort(); + }; }, [ placeInfo.value.enclosingPlace.dcid, placeInfo.value.enclosedPlaceType, diff --git a/static/js/tools/map/fetcher/map_point_coordinate.ts b/static/js/tools/map/fetcher/map_point_coordinate.ts index 83d53486bc..f7c750dfc6 100644 --- a/static/js/tools/map/fetcher/map_point_coordinate.ts +++ b/static/js/tools/map/fetcher/map_point_coordinate.ts @@ -36,6 +36,10 @@ export function useFetchMapPointCoordinate( if (!contextOk) { return; } + // Use AbortController to cancel the request if the component unmounts + const abortController = new AbortController(); + const signal = abortController.signal; + const action: ChartStoreAction = { type: ChartDataType.MAP_POINT_COORDINATE, error: null, @@ -56,6 +60,7 @@ export function useFetchMapPointCoordinate( placeType: placeInfo.value.mapPointPlaceType, }, paramsSerializer: stringifyFn, + signal, }) .then((resp) => { if (resp.status !== 200) { @@ -66,10 +71,19 @@ export function useFetchMapPointCoordinate( console.log("[Map Fetch] map point coordinate"); dispatch(action); }) - .catch(() => { + .catch((error) => { + // Ignore abort errors + if (axios.isCancel(error) || error.name === "AbortError") { + return; + } action.error = "error fetching map point coordinate data"; dispatch(action); }); + + return () => { + // Cancel request if component unmounts + abortController.abort(); + }; }, [ placeInfo.value.enclosingPlace.dcid, placeInfo.value.mapPointPlaceType, diff --git a/static/js/tools/map/fetcher/map_point_stat.ts b/static/js/tools/map/fetcher/map_point_stat.ts index 5723a2b20c..0999a4d17b 100644 --- a/static/js/tools/map/fetcher/map_point_stat.ts +++ b/static/js/tools/map/fetcher/map_point_stat.ts @@ -18,6 +18,7 @@ * Fetch the map point stat data. */ +import axios from "axios"; import _ from "lodash"; import { Dispatch, useContext, useEffect } from "react"; @@ -40,6 +41,10 @@ export function useFetchMapPointStat( if (!contextOk) { return; } + // Use AbortController to cancel the request if the component unmounts + const abortController = new AbortController(); + const signal = abortController.signal; + const action: ChartStoreAction = { type: ChartDataType.MAP_POINT_STAT, error: null, @@ -68,7 +73,9 @@ export function useFetchMapPointStat( date, null, // alignedVariables null, // facetIds - WEBSITE_SURFACE + WEBSITE_SURFACE, + undefined, // facetSelector + signal ) .then((resp) => { if (_.isEmpty(resp.data[usedSV])) { @@ -82,10 +89,19 @@ export function useFetchMapPointStat( console.log(`[Map Fetch] map point stat for: ${date}`); dispatch(action); }) - .catch(() => { + .catch((error) => { + // Ignore abort errors + if (axios.isCancel(error) || error.name === "AbortError") { + return; + } action.error = "error fetching map point stat data"; dispatch(action); }); + + return () => { + // Cancel request if component unmounts + abortController.abort(); + }; }, [ dateCtx.value, placeInfo.value.enclosingPlace.dcid, diff --git a/static/js/tools/map/fetcher/stat_var_summary.ts b/static/js/tools/map/fetcher/stat_var_summary.ts index 1622bc55e8..c21472fcbc 100644 --- a/static/js/tools/map/fetcher/stat_var_summary.ts +++ b/static/js/tools/map/fetcher/stat_var_summary.ts @@ -36,6 +36,10 @@ export function useFetchStatVarSummary( if (!contextOk) { return; } + // Use AbortController to cancel the request if the component unmounts + const abortController = new AbortController(); + const signal = abortController.signal; + const action: ChartStoreAction = { type: ChartDataType.STAT_VAR_SUMMARY, error: null, @@ -51,6 +55,7 @@ export function useFetchStatVarSummary( dcids: [statVar.value.dcid], }, paramsSerializer: stringifyFn, + signal, }) .then((resp) => { if (_.isEmpty(resp.data)) { @@ -61,9 +66,18 @@ export function useFetchStatVarSummary( console.log("[Map Fetch] stat var summary"); dispatch(action); }) - .catch(() => { + .catch((error) => { + // Ignore abort errors + if (axios.isCancel(error) || error.name === "AbortError") { + return; + } action.error = "error fetching stat var summary data"; dispatch(action); }); + + return () => { + // Cancel request if component unmounts + abortController.abort(); + }; }, [statVar.value.dcid, display.value.showTimeSlider, dispatch]); } diff --git a/static/js/utils/data_fetch_utils.ts b/static/js/utils/data_fetch_utils.ts index eee75213fd..c1a9a32e0a 100644 --- a/static/js/utils/data_fetch_utils.ts +++ b/static/js/utils/data_fetch_utils.ts @@ -327,11 +327,12 @@ export function getSeries( variables: string[], facetIds?: string[], facetSelector?: FacetSelectionCriteria, - surface?: string + surface?: string, + signal?: AbortSignal ): Promise { const params = { entities, variables }; return Promise.resolve( - selectFacet(apiRoot, entities, variables, facetSelector, surface) + selectFacet(apiRoot, entities, variables, facetSelector, surface, signal) ).then((resolvedFacetIds) => { if (!_.isEmpty(facetIds)) { params["facetIds"] = facetIds; @@ -342,6 +343,7 @@ export function getSeries( return axios .post(`${apiRoot || ""}/api/observations/series`, params, { headers: getSurfaceHeader(surface), + signal, }) .then((resp) => resp.data); }); @@ -366,7 +368,8 @@ export function getSeriesWithin( childType: string, variables: string[], facetIds?: string[], - surface?: string + surface?: string, + signal?: AbortSignal ): Promise { const params = { parentEntity, childType, variables }; if (facetIds) { @@ -377,6 +380,7 @@ export function getSeriesWithin( params, paramsSerializer: stringifyFn, headers: getSurfaceHeader(surface), + signal, }) .then((resp) => resp.data); } From c11c53fac6f70bca5917c895018ab9434c5a7dab Mon Sep 17 00:00:00 2001 From: Julia Wu Date: Wed, 21 Jan 2026 16:54:51 -0800 Subject: [PATCH 4/8] lint --- static/js/utils/data_fetch_utils.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/static/js/utils/data_fetch_utils.ts b/static/js/utils/data_fetch_utils.ts index c1a9a32e0a..4effedba04 100644 --- a/static/js/utils/data_fetch_utils.ts +++ b/static/js/utils/data_fetch_utils.ts @@ -218,14 +218,7 @@ export function getPoint( ): Promise { const facetPromise = !_.isEmpty(facetIds) ? Promise.resolve(facetIds) - : selectFacet( - apiRoot, - entities, - variables, - facetSelector, - surface, - signal - ); + : selectFacet(apiRoot, entities, variables, facetSelector, surface, signal); return facetPromise.then((resolvedFacetIds) => { const params: Record = { date, entities, variables }; From 8923d81a6da5b4b2389a314d497e8a3a48da22c8 Mon Sep 17 00:00:00 2001 From: Julia Wu Date: Thu, 22 Jan 2026 09:51:21 -0800 Subject: [PATCH 5/8] standardize format of ignoring abort errors --- static/js/stat_var_hierarchy/stat_var_group_node.tsx | 2 +- static/js/stat_var_hierarchy/stat_var_hierarchy.tsx | 7 ++++--- static/js/tools/map/fetcher/all_dates.ts | 4 ++-- static/js/tools/map/fetcher/all_stat.ts | 4 ++-- static/js/tools/map/fetcher/border_geojson.ts | 4 ++-- static/js/tools/map/fetcher/breadcrumb_denom_stat.ts | 4 ++-- static/js/tools/map/fetcher/breadcrumb_stat.ts | 4 ++-- static/js/tools/map/fetcher/default_stat.ts | 4 ++-- static/js/tools/map/fetcher/denom_stat.ts | 4 ++-- static/js/tools/map/fetcher/geojson.ts | 2 +- static/js/tools/map/fetcher/map_point_coordinate.ts | 4 ++-- static/js/tools/map/fetcher/map_point_stat.ts | 4 ++-- static/js/tools/map/fetcher/stat_var_summary.ts | 4 ++-- 13 files changed, 26 insertions(+), 25 deletions(-) diff --git a/static/js/stat_var_hierarchy/stat_var_group_node.tsx b/static/js/stat_var_hierarchy/stat_var_group_node.tsx index ccef900079..561ca905bb 100644 --- a/static/js/stat_var_hierarchy/stat_var_group_node.tsx +++ b/static/js/stat_var_hierarchy/stat_var_group_node.tsx @@ -311,8 +311,8 @@ export class StatVarGroupNode extends React.Component< } }) .catch((error) => { - // Ignore abort errors if (axios.isCancel(error) || error.name === "AbortError") { + // Ignore abort errors return; } this.dataFetchingEntities = null; diff --git a/static/js/stat_var_hierarchy/stat_var_hierarchy.tsx b/static/js/stat_var_hierarchy/stat_var_hierarchy.tsx index beaf53d684..472347eeb6 100644 --- a/static/js/stat_var_hierarchy/stat_var_hierarchy.tsx +++ b/static/js/stat_var_hierarchy/stat_var_hierarchy.tsx @@ -376,10 +376,11 @@ export class StatVarHierarchy extends React.Component< this.togglePath(selection, path, /*isSearchSelection=*/ true); }) .catch((error) => { - // Ignore abort errors - if (!axios.isCancel(error) && error.name !== "AbortError") { - console.error(error); + if (axios.isCancel(error) || error.name === "AbortError") { + // Ignore abort errors + return; } + console.error(error); }); } diff --git a/static/js/tools/map/fetcher/all_dates.ts b/static/js/tools/map/fetcher/all_dates.ts index b8241b7d7a..c16a29fad5 100644 --- a/static/js/tools/map/fetcher/all_dates.ts +++ b/static/js/tools/map/fetcher/all_dates.ts @@ -83,9 +83,9 @@ export function useFetchAllDates(dispatch: Dispatch): void { dispatch(action); }) .catch((error) => { - // Ignore abort errors if (axios.isCancel(error) || error.name === "AbortError") { - return; + // Ignore abort errors + return; } action.error = "error fetching all the dates"; dispatch(action); diff --git a/static/js/tools/map/fetcher/all_stat.ts b/static/js/tools/map/fetcher/all_stat.ts index be9a457805..9e8ef9ddf1 100644 --- a/static/js/tools/map/fetcher/all_stat.ts +++ b/static/js/tools/map/fetcher/all_stat.ts @@ -91,9 +91,9 @@ export function useFetchAllStat(dispatch: Dispatch): void { dispatch(action); }) .catch((error) => { - // Ignore abort errors if (axios.isCancel(error) || error.name === "AbortError") { - return; + // Ignore abort errors + return; } action.error = "error fetching all stat data"; dispatch(action); diff --git a/static/js/tools/map/fetcher/border_geojson.ts b/static/js/tools/map/fetcher/border_geojson.ts index ff0d45069b..cfa027e770 100644 --- a/static/js/tools/map/fetcher/border_geojson.ts +++ b/static/js/tools/map/fetcher/border_geojson.ts @@ -80,9 +80,9 @@ export function useFetchBorderGeoJson( dispatch(action); }) .catch((error) => { - // Ignore abort errors if (axios.isCancel(error) || error.name === "AbortError") { - return; + // Ignore abort errors + return; } action.error = "error fetching border geo json data"; dispatch(action); diff --git a/static/js/tools/map/fetcher/breadcrumb_denom_stat.ts b/static/js/tools/map/fetcher/breadcrumb_denom_stat.ts index 4ba461dede..758f32ad82 100644 --- a/static/js/tools/map/fetcher/breadcrumb_denom_stat.ts +++ b/static/js/tools/map/fetcher/breadcrumb_denom_stat.ts @@ -88,9 +88,9 @@ export function useFetchBreadcrumbDenomStat( dispatch(action); }) .catch((error) => { - // Ignore abort errors if (axios.isCancel(error) || error.name === "AbortError") { - return; + // Ignore abort errors + return; } action.error = "error fetching breadcrumb denom stat data"; dispatch(action); diff --git a/static/js/tools/map/fetcher/breadcrumb_stat.ts b/static/js/tools/map/fetcher/breadcrumb_stat.ts index 9b7ad00674..d7e24534a2 100644 --- a/static/js/tools/map/fetcher/breadcrumb_stat.ts +++ b/static/js/tools/map/fetcher/breadcrumb_stat.ts @@ -94,9 +94,9 @@ export function useFetchBreadcrumbStat( dispatch(action); }) .catch((error) => { - // Ignore abort errors if (axios.isCancel(error) || error.name === "AbortError") { - return; + // Ignore abort errors + return; } action.error = "error fetching breadcrumb stat data"; dispatch(action); diff --git a/static/js/tools/map/fetcher/default_stat.ts b/static/js/tools/map/fetcher/default_stat.ts index b525fe5fb6..627a33e7cb 100644 --- a/static/js/tools/map/fetcher/default_stat.ts +++ b/static/js/tools/map/fetcher/default_stat.ts @@ -88,9 +88,9 @@ export function useFetchDefaultStat( dispatch(action); }) .catch((error) => { - // Ignore abort errors if (axios.isCancel(error) || error.name === "AbortError") { - return; + // Ignore abort errors + return; } action.error = "error fetching default stat data"; dispatch(action); diff --git a/static/js/tools/map/fetcher/denom_stat.ts b/static/js/tools/map/fetcher/denom_stat.ts index 0e98e94e6e..0ee1c78823 100644 --- a/static/js/tools/map/fetcher/denom_stat.ts +++ b/static/js/tools/map/fetcher/denom_stat.ts @@ -80,9 +80,9 @@ export function useFetchDenomStat(dispatch: Dispatch): void { dispatch(action); }) .catch((error) => { - // Ignore abort errors if (axios.isCancel(error) || error.name === "AbortError") { - return; + // Ignore abort errors + return; } action.error = "error fetching denom stat data"; dispatch(action); diff --git a/static/js/tools/map/fetcher/geojson.ts b/static/js/tools/map/fetcher/geojson.ts index b4243547e3..5a1137955c 100644 --- a/static/js/tools/map/fetcher/geojson.ts +++ b/static/js/tools/map/fetcher/geojson.ts @@ -69,8 +69,8 @@ export function useFetchGeoJson(dispatch: Dispatch): void { dispatch(action); }) .catch((error) => { - // Ignore abort errors if (axios.isCancel(error) || error.name === "AbortError") { + // Ignore abort errors return; } action.error = "error fetching geo json data"; diff --git a/static/js/tools/map/fetcher/map_point_coordinate.ts b/static/js/tools/map/fetcher/map_point_coordinate.ts index f7c750dfc6..e01c75efbc 100644 --- a/static/js/tools/map/fetcher/map_point_coordinate.ts +++ b/static/js/tools/map/fetcher/map_point_coordinate.ts @@ -72,9 +72,9 @@ export function useFetchMapPointCoordinate( dispatch(action); }) .catch((error) => { - // Ignore abort errors if (axios.isCancel(error) || error.name === "AbortError") { - return; + // Ignore abort errors + return; } action.error = "error fetching map point coordinate data"; dispatch(action); diff --git a/static/js/tools/map/fetcher/map_point_stat.ts b/static/js/tools/map/fetcher/map_point_stat.ts index 0999a4d17b..3f3ba1e631 100644 --- a/static/js/tools/map/fetcher/map_point_stat.ts +++ b/static/js/tools/map/fetcher/map_point_stat.ts @@ -90,9 +90,9 @@ export function useFetchMapPointStat( dispatch(action); }) .catch((error) => { - // Ignore abort errors if (axios.isCancel(error) || error.name === "AbortError") { - return; + // Ignore abort errors + return; } action.error = "error fetching map point stat data"; dispatch(action); diff --git a/static/js/tools/map/fetcher/stat_var_summary.ts b/static/js/tools/map/fetcher/stat_var_summary.ts index c21472fcbc..1bdbea840a 100644 --- a/static/js/tools/map/fetcher/stat_var_summary.ts +++ b/static/js/tools/map/fetcher/stat_var_summary.ts @@ -67,9 +67,9 @@ export function useFetchStatVarSummary( dispatch(action); }) .catch((error) => { - // Ignore abort errors if (axios.isCancel(error) || error.name === "AbortError") { - return; + // Ignore abort errors + return; } action.error = "error fetching stat var summary data"; dispatch(action); From 347a63f81f1b5766989b2d54d78948d1fd48e516 Mon Sep 17 00:00:00 2001 From: Julia Wu Date: Thu, 22 Jan 2026 09:53:04 -0800 Subject: [PATCH 6/8] lint --- static/js/tools/map/fetcher/all_dates.ts | 2 +- static/js/tools/map/fetcher/all_stat.ts | 2 +- static/js/tools/map/fetcher/border_geojson.ts | 2 +- static/js/tools/map/fetcher/breadcrumb_denom_stat.ts | 2 +- static/js/tools/map/fetcher/breadcrumb_stat.ts | 2 +- static/js/tools/map/fetcher/default_stat.ts | 2 +- static/js/tools/map/fetcher/denom_stat.ts | 2 +- static/js/tools/map/fetcher/map_point_coordinate.ts | 2 +- static/js/tools/map/fetcher/map_point_stat.ts | 2 +- static/js/tools/map/fetcher/stat_var_summary.ts | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/static/js/tools/map/fetcher/all_dates.ts b/static/js/tools/map/fetcher/all_dates.ts index c16a29fad5..26c49438a3 100644 --- a/static/js/tools/map/fetcher/all_dates.ts +++ b/static/js/tools/map/fetcher/all_dates.ts @@ -85,7 +85,7 @@ export function useFetchAllDates(dispatch: Dispatch): void { .catch((error) => { if (axios.isCancel(error) || error.name === "AbortError") { // Ignore abort errors - return; + return; } action.error = "error fetching all the dates"; dispatch(action); diff --git a/static/js/tools/map/fetcher/all_stat.ts b/static/js/tools/map/fetcher/all_stat.ts index 9e8ef9ddf1..a6ac4055cd 100644 --- a/static/js/tools/map/fetcher/all_stat.ts +++ b/static/js/tools/map/fetcher/all_stat.ts @@ -93,7 +93,7 @@ export function useFetchAllStat(dispatch: Dispatch): void { .catch((error) => { if (axios.isCancel(error) || error.name === "AbortError") { // Ignore abort errors - return; + return; } action.error = "error fetching all stat data"; dispatch(action); diff --git a/static/js/tools/map/fetcher/border_geojson.ts b/static/js/tools/map/fetcher/border_geojson.ts index cfa027e770..4176d5913f 100644 --- a/static/js/tools/map/fetcher/border_geojson.ts +++ b/static/js/tools/map/fetcher/border_geojson.ts @@ -82,7 +82,7 @@ export function useFetchBorderGeoJson( .catch((error) => { if (axios.isCancel(error) || error.name === "AbortError") { // Ignore abort errors - return; + return; } action.error = "error fetching border geo json data"; dispatch(action); diff --git a/static/js/tools/map/fetcher/breadcrumb_denom_stat.ts b/static/js/tools/map/fetcher/breadcrumb_denom_stat.ts index 758f32ad82..6d11560048 100644 --- a/static/js/tools/map/fetcher/breadcrumb_denom_stat.ts +++ b/static/js/tools/map/fetcher/breadcrumb_denom_stat.ts @@ -90,7 +90,7 @@ export function useFetchBreadcrumbDenomStat( .catch((error) => { if (axios.isCancel(error) || error.name === "AbortError") { // Ignore abort errors - return; + return; } action.error = "error fetching breadcrumb denom stat data"; dispatch(action); diff --git a/static/js/tools/map/fetcher/breadcrumb_stat.ts b/static/js/tools/map/fetcher/breadcrumb_stat.ts index d7e24534a2..e6c3b87f9b 100644 --- a/static/js/tools/map/fetcher/breadcrumb_stat.ts +++ b/static/js/tools/map/fetcher/breadcrumb_stat.ts @@ -96,7 +96,7 @@ export function useFetchBreadcrumbStat( .catch((error) => { if (axios.isCancel(error) || error.name === "AbortError") { // Ignore abort errors - return; + return; } action.error = "error fetching breadcrumb stat data"; dispatch(action); diff --git a/static/js/tools/map/fetcher/default_stat.ts b/static/js/tools/map/fetcher/default_stat.ts index 627a33e7cb..2701399182 100644 --- a/static/js/tools/map/fetcher/default_stat.ts +++ b/static/js/tools/map/fetcher/default_stat.ts @@ -90,7 +90,7 @@ export function useFetchDefaultStat( .catch((error) => { if (axios.isCancel(error) || error.name === "AbortError") { // Ignore abort errors - return; + return; } action.error = "error fetching default stat data"; dispatch(action); diff --git a/static/js/tools/map/fetcher/denom_stat.ts b/static/js/tools/map/fetcher/denom_stat.ts index 0ee1c78823..8d24fc8152 100644 --- a/static/js/tools/map/fetcher/denom_stat.ts +++ b/static/js/tools/map/fetcher/denom_stat.ts @@ -82,7 +82,7 @@ export function useFetchDenomStat(dispatch: Dispatch): void { .catch((error) => { if (axios.isCancel(error) || error.name === "AbortError") { // Ignore abort errors - return; + return; } action.error = "error fetching denom stat data"; dispatch(action); diff --git a/static/js/tools/map/fetcher/map_point_coordinate.ts b/static/js/tools/map/fetcher/map_point_coordinate.ts index e01c75efbc..f7fa2372f3 100644 --- a/static/js/tools/map/fetcher/map_point_coordinate.ts +++ b/static/js/tools/map/fetcher/map_point_coordinate.ts @@ -74,7 +74,7 @@ export function useFetchMapPointCoordinate( .catch((error) => { if (axios.isCancel(error) || error.name === "AbortError") { // Ignore abort errors - return; + return; } action.error = "error fetching map point coordinate data"; dispatch(action); diff --git a/static/js/tools/map/fetcher/map_point_stat.ts b/static/js/tools/map/fetcher/map_point_stat.ts index 3f3ba1e631..19fea042ec 100644 --- a/static/js/tools/map/fetcher/map_point_stat.ts +++ b/static/js/tools/map/fetcher/map_point_stat.ts @@ -92,7 +92,7 @@ export function useFetchMapPointStat( .catch((error) => { if (axios.isCancel(error) || error.name === "AbortError") { // Ignore abort errors - return; + return; } action.error = "error fetching map point stat data"; dispatch(action); diff --git a/static/js/tools/map/fetcher/stat_var_summary.ts b/static/js/tools/map/fetcher/stat_var_summary.ts index 1bdbea840a..9363bcb69a 100644 --- a/static/js/tools/map/fetcher/stat_var_summary.ts +++ b/static/js/tools/map/fetcher/stat_var_summary.ts @@ -69,7 +69,7 @@ export function useFetchStatVarSummary( .catch((error) => { if (axios.isCancel(error) || error.name === "AbortError") { // Ignore abort errors - return; + return; } action.error = "error fetching stat var summary data"; dispatch(action); From d93bf5243d918023a71c1c46c5eb872aaff9f261 Mon Sep 17 00:00:00 2001 From: Julia Wu Date: Thu, 22 Jan 2026 10:02:00 -0800 Subject: [PATCH 7/8] use a separate search controller for search requests --- .../stat_var_hierarchy/stat_var_hierarchy.tsx | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/static/js/stat_var_hierarchy/stat_var_hierarchy.tsx b/static/js/stat_var_hierarchy/stat_var_hierarchy.tsx index 472347eeb6..82f6a38ab7 100644 --- a/static/js/stat_var_hierarchy/stat_var_hierarchy.tsx +++ b/static/js/stat_var_hierarchy/stat_var_hierarchy.tsx @@ -100,8 +100,10 @@ export class StatVarHierarchy extends React.Component< StatVarHierarchyPropType, StatVarHierarchyStateType > { - // Abort controller to cancel any in-flight requests on re-render - private _abortController: AbortController; + // Abort controller to cancel any in-flight data requests + private _dataAbortController: AbortController; + // Abort controller to cancel any in-flight search requests + private _searchAbortController: AbortController; constructor(props: StatVarHierarchyPropType) { super(props); @@ -125,9 +127,13 @@ export class StatVarHierarchy extends React.Component< } componentWillUnmount(): void { - if (this._abortController) { - // Abort the any requests generated by the component we are unmounting - this._abortController.abort(); + if (this._dataAbortController) { + // Abort any in-flight data requests + this._dataAbortController.abort(); + } + if (this._searchAbortController) { + // Abort any in-flight search requests + this._searchAbortController.abort(); } } @@ -267,12 +273,12 @@ export class StatVarHierarchy extends React.Component< } private async fetchData(): Promise { - if (this._abortController) { - // Abort the previous request - this._abortController.abort(); + if (this._dataAbortController) { + // Abort the previous data request + this._dataAbortController.abort(); } - this._abortController = new AbortController(); - const signal = this._abortController.signal; + this._dataAbortController = new AbortController(); + const signal = this._dataAbortController.signal; loadSpinner(SV_HIERARCHY_SECTION_ID); const entityList = this.props.entities.map((entity) => entity.dcid); @@ -363,7 +369,11 @@ export class StatVarHierarchy extends React.Component< } private onSearchSelectionChange(selection: string): void { - this.getPath(selection, this._abortController?.signal) + if (this._searchAbortController) { + this._searchAbortController.abort(); + } + this._searchAbortController = new AbortController(); + this.getPath(selection, this._searchAbortController.signal) .then((path) => { const searchSelectionCleared = !_.isEmpty(this.state.focusPath) && _.isEmpty(path); From 77cf179de7507e4eb521c85d9b94e690883a0848 Mon Sep 17 00:00:00 2001 From: Julia Wu Date: Mon, 26 Jan 2026 11:22:53 -0800 Subject: [PATCH 8/8] Update some tests --- .../stat_var_hierarchy/stat_var_hierarchy.tsx | 6 + static/js/tools/scatter/app.test.tsx | 424 +++++++++++------- 2 files changed, 270 insertions(+), 160 deletions(-) diff --git a/static/js/stat_var_hierarchy/stat_var_hierarchy.tsx b/static/js/stat_var_hierarchy/stat_var_hierarchy.tsx index 82f6a38ab7..f209299e9f 100644 --- a/static/js/stat_var_hierarchy/stat_var_hierarchy.tsx +++ b/static/js/stat_var_hierarchy/stat_var_hierarchy.tsx @@ -440,6 +440,12 @@ export class StatVarHierarchy extends React.Component< // This is to make jest test working, should find a better way to let // mock return new object each time. return _.cloneDeep(resp.data).reverse(); + }).catch((error) => { + if (axios.isCancel(error) || error.name === "AbortError") { + // Ignore abort errors + return; + } + console.error(error); }); } diff --git a/static/js/tools/scatter/app.test.tsx b/static/js/tools/scatter/app.test.tsx index 8560b3f41c..ec16ac4683 100644 --- a/static/js/tools/scatter/app.test.tsx +++ b/static/js/tools/scatter/app.test.tsx @@ -285,16 +285,20 @@ function mockAxios(): void { }; when(axios.get) - .calledWith("/api/observations/point/within", { - params: { - parentEntity: "geoId/10", - childType: "County", - variables: ["Count_Person_Employed"], - date: "", - }, - paramsSerializer: stringifyFn, - headers: WEBSITE_SURFACE_HEADER, - }) + .calledWith( + "/api/observations/point/within", + { + params: { + parentEntity: "geoId/10", + childType: "County", + variables: ["Count_Person_Employed"], + date: "", + }, + paramsSerializer: stringifyFn, + headers: WEBSITE_SURFACE_HEADER, + }, + expect.anything() // mock abort signal + ) .mockResolvedValue({ data: { data: { @@ -304,16 +308,20 @@ function mockAxios(): void { }, }); when(axios.get) - .calledWith("/api/observations/point/within", { - params: { - parentEntity: "geoId/10", - childType: "County", - variables: ["Count_HousingUnit"], - date: "", - }, - paramsSerializer: stringifyFn, - headers: WEBSITE_SURFACE_HEADER, - }) + .calledWith( + "/api/observations/point/within", + { + params: { + parentEntity: "geoId/10", + childType: "County", + variables: ["Count_HousingUnit"], + date: "", + }, + paramsSerializer: stringifyFn, + headers: WEBSITE_SURFACE_HEADER, + }, + expect.anything() // mock abort signal + ) .mockResolvedValue({ data: { data: { @@ -323,16 +331,20 @@ function mockAxios(): void { }, }); when(axios.get) - .calledWith("/api/observations/point/within", { - params: { - parentEntity: "geoId/10", - childType: "County", - variables: ["Count_Establishment"], - date: "", - }, - paramsSerializer: stringifyFn, - headers: WEBSITE_SURFACE_HEADER, - }) + .calledWith( + "/api/observations/point/within", + { + params: { + parentEntity: "geoId/10", + childType: "County", + variables: ["Count_Establishment"], + date: "", + }, + paramsSerializer: stringifyFn, + headers: WEBSITE_SURFACE_HEADER, + }, + expect.anything() // mock abort signal + ) .mockResolvedValue({ data: { data: { @@ -342,16 +354,20 @@ function mockAxios(): void { }, }); when(axios.get) - .calledWith("/api/observations/point/within/all", { - params: { - parentEntity: "geoId/10", - childType: "County", - variables: ["Count_Person_Employed"], - date: "", - }, - paramsSerializer: stringifyFn, - headers: WEBSITE_SURFACE_HEADER, - }) + .calledWith( + "/api/observations/point/within/all", + { + params: { + parentEntity: "geoId/10", + childType: "County", + variables: ["Count_Person_Employed"], + date: "", + }, + paramsSerializer: stringifyFn, + headers: WEBSITE_SURFACE_HEADER, + }, + expect.anything() // mock abort signal + ) .mockResolvedValue({ data: { data: { @@ -361,16 +377,20 @@ function mockAxios(): void { }, }); when(axios.get) - .calledWith("/api/observations/point/within/all", { - params: { - parentEntity: "geoId/10", - childType: "County", - variables: ["Count_HousingUnit"], - date: "", - }, - paramsSerializer: stringifyFn, - headers: WEBSITE_SURFACE_HEADER, - }) + .calledWith( + "/api/observations/point/within/all", + { + params: { + parentEntity: "geoId/10", + childType: "County", + variables: ["Count_HousingUnit"], + date: "", + }, + paramsSerializer: stringifyFn, + headers: WEBSITE_SURFACE_HEADER, + }, + expect.anything() // mock abort signal + ) .mockResolvedValue({ data: { data: { @@ -380,16 +400,20 @@ function mockAxios(): void { }, }); when(axios.get) - .calledWith("/api/observations/point/within/all", { - params: { - parentEntity: "geoId/10", - childType: "County", - variables: ["Count_Establishment"], - date: "", - }, - paramsSerializer: stringifyFn, - headers: WEBSITE_SURFACE_HEADER, - }) + .calledWith( + "/api/observations/point/within/all", + { + params: { + parentEntity: "geoId/10", + childType: "County", + variables: ["Count_Establishment"], + date: "", + }, + paramsSerializer: stringifyFn, + headers: WEBSITE_SURFACE_HEADER, + }, + expect.anything() // mock abort signal + ) .mockResolvedValue({ data: { data: { @@ -400,14 +424,18 @@ function mockAxios(): void { }); when(axios.get) - .calledWith("/api/observations/point", { - params: { - variables: ["Count_Person"], - entities: ["geoId/10001", "geoId/10003", "geoId/10005"], + .calledWith( + "/api/observations/point", + { + params: { + variables: ["Count_Person"], + entities: ["geoId/10001", "geoId/10003", "geoId/10005"], + }, + paramsSerializer: stringifyFn, + headers: WEBSITE_SURFACE_HEADER, }, - paramsSerializer: stringifyFn, - headers: WEBSITE_SURFACE_HEADER, - }) + expect.anything() // mock abort signal + ) .mockResolvedValue({ data: { data: { @@ -430,15 +458,19 @@ function mockAxios(): void { }); when(axios.get) - .calledWith("/api/observations/series/within", { - params: { - parentEntity: "geoId/10", - childType: "County", - variables: ["Count_Person"], - }, - paramsSerializer: stringifyFn, - headers: WEBSITE_SURFACE_HEADER, - }) + .calledWith( + "/api/observations/series/within", + { + params: { + parentEntity: "geoId/10", + childType: "County", + variables: ["Count_Person"], + }, + paramsSerializer: stringifyFn, + headers: WEBSITE_SURFACE_HEADER, + }, + expect.anything() // mock abort signal + ) .mockResolvedValue({ data: { data: { @@ -509,107 +541,159 @@ function mockAxios(): void { }); when(axios.post) - .calledWith("/api/variable-group/info", { - dcid: "dc/g/Root", - entities: [], - numEntitiesExistence: 0, - }) + .calledWith( + "/api/variable-group/info", + { + dcid: "dc/g/Root", + entities: [], + numEntitiesExistence: 0, + }, + expect.anything() // mock abort signal + ) .mockResolvedValue(rootGroupsData); when(axios.post) - .calledWith("/api/variable-group/info", { - dcid: "dc/g/Root", - entities: ["geoId/10001", "geoId/10003", "geoId/10005"], - numEntitiesExistence: 1, - }) + .calledWith( + "/api/variable-group/info", + { + dcid: "dc/g/Root", + entities: ["geoId/10001", "geoId/10003", "geoId/10005"], + numEntitiesExistence: 1, + }, + expect.anything() // mock abort signal + ) .mockResolvedValue(rootGroupsData); when(axios.post) - .calledWith("/api/variable-group/info", { - dcid: "dc/g/Root", - entities: ["geoId/10001", "geoId/10005", "geoId/10003"], - numEntitiesExistence: 1, - }) + .calledWith( + "/api/variable-group/info", + { + dcid: "dc/g/Root", + entities: ["geoId/10001", "geoId/10005", "geoId/10003"], + numEntitiesExistence: 1, + }, + expect.anything() // mock abort signal + ) .mockResolvedValue(rootGroupsData); when(axios.post) - .calledWith("/api/variable-group/info", { - dcid: "dc/g/Root", - entities: ["geoId/10003", "geoId/10001", "geoId/10005"], - numEntitiesExistence: 1, - }) + .calledWith( + "/api/variable-group/info", + { + dcid: "dc/g/Root", + entities: ["geoId/10003", "geoId/10001", "geoId/10005"], + numEntitiesExistence: 1, + }, + expect.anything() // mock abort signal + ) .mockResolvedValue(rootGroupsData); when(axios.post) - .calledWith("/api/variable-group/info", { - dcid: "dc/g/Root", - entities: ["geoId/10003", "geoId/10005", "geoId/10001"], - numEntitiesExistence: 1, - }) + .calledWith( + "/api/variable-group/info", + { + dcid: "dc/g/Root", + entities: ["geoId/10003", "geoId/10005", "geoId/10001"], + numEntitiesExistence: 1, + }, + expect.anything() // mock abort signal + ) .mockResolvedValue(rootGroupsData); when(axios.post) - .calledWith("/api/variable-group/info", { - dcid: "dc/g/Root", - entities: ["geoId/10005", "geoId/10003", "geoId/10001"], - numEntitiesExistence: 1, - }) + .calledWith( + "/api/variable-group/info", + { + dcid: "dc/g/Root", + entities: ["geoId/10005", "geoId/10003", "geoId/10001"], + numEntitiesExistence: 1, + }, + expect.anything() // mock abort signal + ) .mockResolvedValue(rootGroupsData); when(axios.post) - .calledWith("/api/variable-group/info", { - dcid: "dc/g/Root", - entities: ["geoId/10005", "geoId/10001", "geoId/10003"], - numEntitiesExistence: 1, - }) + .calledWith( + "/api/variable-group/info", + { + dcid: "dc/g/Root", + entities: ["geoId/10005", "geoId/10001", "geoId/10003"], + numEntitiesExistence: 1, + }, + expect.anything() // mock abort signal + ) .mockResolvedValue(rootGroupsData); when(axios.post) - .calledWith("/api/variable-group/info", { - dcid: "dc/g/Demographics", - entities: ["geoId/10001", "geoId/10003", "geoId/10005"], - numEntitiesExistence: 1, - }) + .calledWith( + "/api/variable-group/info", + { + dcid: "dc/g/Demographics", + entities: ["geoId/10001", "geoId/10003", "geoId/10005"], + numEntitiesExistence: 1, + }, + expect.anything() // mock abort signal + ) .mockResolvedValue(demographicsGroupsData); when(axios.post) - .calledWith("/api/variable-group/info", { - dcid: "dc/g/Demographics", - entities: ["geoId/10001", "geoId/10005", "geoId/10003"], - numEntitiesExistence: 1, - }) + .calledWith( + "/api/variable-group/info", + { + dcid: "dc/g/Demographics", + entities: ["geoId/10001", "geoId/10005", "geoId/10003"], + numEntitiesExistence: 1, + }, + expect.anything() // mock abort signal + ) .mockResolvedValue(demographicsGroupsData); when(axios.post) - .calledWith("/api/variable-group/info", { - dcid: "dc/g/Demographics", - entities: ["geoId/10003", "geoId/10001", "geoId/10005"], - numEntitiesExistence: 1, - }) + .calledWith( + "/api/variable-group/info", + { + dcid: "dc/g/Demographics", + entities: ["geoId/10003", "geoId/10001", "geoId/10005"], + numEntitiesExistence: 1, + }, + expect.anything() // mock abort signal + ) .mockResolvedValue(demographicsGroupsData); when(axios.post) - .calledWith("/api/variable-group/info", { - dcid: "dc/g/Demographics", - entities: ["geoId/10003", "geoId/10005", "geoId/10001"], - numEntitiesExistence: 1, - }) + .calledWith( + "/api/variable-group/info", + { + dcid: "dc/g/Demographics", + entities: ["geoId/10003", "geoId/10005", "geoId/10001"], + numEntitiesExistence: 1, + }, + expect.anything() // mock abort signal + ) .mockResolvedValue(demographicsGroupsData); when(axios.post) - .calledWith("/api/variable-group/info", { - dcid: "dc/g/Demographics", - entities: ["geoId/10005", "geoId/10003", "geoId/10001"], - numEntitiesExistence: 1, - }) + .calledWith( + "/api/variable-group/info", + { + dcid: "dc/g/Demographics", + entities: ["geoId/10005", "geoId/10003", "geoId/10001"], + numEntitiesExistence: 1, + }, + expect.anything() // mock abort signal + ) .mockResolvedValue(demographicsGroupsData); when(axios.post) - .calledWith("/api/variable-group/info", { - dcid: "dc/g/Demographics", - entities: ["geoId/10005", "geoId/10001", "geoId/10003"], - numEntitiesExistence: 1, - }) + .calledWith( + "/api/variable-group/info", + { + dcid: "dc/g/Demographics", + entities: ["geoId/10005", "geoId/10001", "geoId/10003"], + numEntitiesExistence: 1, + }, + expect.anything() // mock abort signal + ) .mockResolvedValue(demographicsGroupsData); when(axios.get) @@ -625,28 +709,41 @@ function mockAxios(): void { .mockResolvedValue(statVarInfoData); when(axios.get) - .calledWith("/api/variable/path?dcid=Count_Establishment") + .calledWith( + "/api/variable/path?dcid=Count_Establishment", + expect.anything() // mock abort signal + ) .mockResolvedValue({ data: pathsData.Count_Establishment }); when(axios.get) - .calledWith("/api/variable/path?dcid=Count_HousingUnit") + .calledWith( + "/api/variable/path?dcid=Count_HousingUnit", + expect.anything() // mock abort signal + ) .mockResolvedValue({ data: pathsData.Count_HousingUnit }); when(axios.get) - .calledWith("/api/variable/path?dcid=Count_Person_Employed") + .calledWith( + "/api/variable/path?dcid=Count_Person_Employed", + expect.anything() // mock abort signal + ) .mockResolvedValue({ data: pathsData.Count_Person_Employed }); when(axios.get) - .calledWith("/api/variable/info", { - params: { - dcids: [ - "Count_Person_Employed", - "Count_HousingUnit", - "Count_Establishment", - ], - }, - paramsSerializer: stringifyFn, - }) + .calledWith( + "/api/variable/info", + { + params: { + dcids: [ + "Count_Person_Employed", + "Count_HousingUnit", + "Count_Establishment", + ], + }, + paramsSerializer: stringifyFn, + }, + expect.anything() // mock abort signal + ) .mockResolvedValue({ data: { Count_Person_Employed: { @@ -677,7 +774,10 @@ function mockAxios(): void { }); when(axios.get) - .calledWith("/api/choropleth/geojson?placeDcid=geoId/10&placeType=County") + .calledWith( + "/api/choropleth/geojson?placeDcid=geoId/10&placeType=County", + expect.anything() // mock abort signal + ) .mockResolvedValue({ features: [], type: "FeatureCollection", @@ -709,11 +809,15 @@ test("all functionalities", async () => { mockAxios(); const app = mount(); await waitFor(() => { - expect(axios.post).toHaveBeenCalledWith("/api/variable-group/info", { - dcid: "dc/g/Root", - entities: [], - numEntitiesExistence: 0, - }); + expect(axios.post).toHaveBeenCalledWith( + "/api/variable-group/info", + { + dcid: "dc/g/Root", + entities: [], + numEntitiesExistence: 0, + }, + expect.anything() // mock abort signal + ); }); await app.update();