diff --git a/common/changes/@itwin/imodel-browser-react/itwin-multiple-request_2024-12-05-17-57.json b/common/changes/@itwin/imodel-browser-react/itwin-multiple-request_2024-12-05-17-57.json new file mode 100644 index 00000000..c3fdc96c --- /dev/null +++ b/common/changes/@itwin/imodel-browser-react/itwin-multiple-request_2024-12-05-17-57.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/imodel-browser-react", + "comment": "Avoid unnecessary refetchs to API", + "type": "patch" + } + ], + "packageName": "@itwin/imodel-browser-react" +} \ No newline at end of file diff --git a/packages/modules/imodel-browser/src/containers/ITwinGrid/ITwinGrid.test.tsx b/packages/modules/imodel-browser/src/containers/ITwinGrid/ITwinGrid.test.tsx index 494533b1..f6ae2a05 100644 --- a/packages/modules/imodel-browser/src/containers/ITwinGrid/ITwinGrid.test.tsx +++ b/packages/modules/imodel-browser/src/containers/ITwinGrid/ITwinGrid.test.tsx @@ -55,4 +55,43 @@ describe("ITwinGrid", () => { expect(wrapper.getByRole("table")).toBeDefined(); expect(wrapper.getAllByRole("row").length).toEqual(3); // First row is header }); + + it("should not refetch iTwins favorites when component rerenders", async () => { + // Arrange + const fetchMore = jest.fn(); + jest.spyOn(useITwinData, "useITwinData").mockReturnValue({ + iTwins: [], + status: DataStatus.Complete, + fetchMore, + }); + // Act + const signal = new AbortController().signal; + const wrapper = render( + + ); + wrapper.rerender( + + ); + + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith( + "https://qa-api.bentley.com/itwins/favorites?subClass=Project", + { + headers: { + Accept: "application/vnd.bentley.itwin-platform.v1+json", + "Cache-Control": "", + authorization: "accessToken", + }, + signal: signal, + } + ); + }); }); diff --git a/packages/modules/imodel-browser/src/containers/ITwinGrid/ITwinGrid.tsx b/packages/modules/imodel-browser/src/containers/ITwinGrid/ITwinGrid.tsx index 25109441..86ea62b5 100644 --- a/packages/modules/imodel-browser/src/containers/ITwinGrid/ITwinGrid.tsx +++ b/packages/modules/imodel-browser/src/containers/ITwinGrid/ITwinGrid.tsx @@ -122,7 +122,7 @@ export const ITwinGrid = ({ removeITwinFromFavorites, shouldRefetchFavorites, resetShouldRefetchFavorites, - } = useITwinFavorites(accessToken, apiOverrides); + } = useITwinFavorites(accessToken, apiOverrides?.serverEnvironmentPrefix); const strings = _mergeStrings( { diff --git a/packages/modules/imodel-browser/src/containers/ITwinGrid/useITwinData.ts b/packages/modules/imodel-browser/src/containers/ITwinGrid/useITwinData.ts index f0f6f4fa..cac36ba4 100644 --- a/packages/modules/imodel-browser/src/containers/ITwinGrid/useITwinData.ts +++ b/packages/modules/imodel-browser/src/containers/ITwinGrid/useITwinData.ts @@ -36,6 +36,7 @@ export const useITwinData = ({ resetShouldRefetchFavorites, }: ProjectDataHookOptions) => { const data = apiOverrides?.data; + const serverEnvironmentPrefix = apiOverrides?.serverEnvironmentPrefix; const [projects, setProjects] = React.useState([]); const [status, setStatus] = React.useState(); const filteredProjects = useITwinFilter(projects, filterOptions); @@ -69,7 +70,7 @@ export const useITwinData = ({ setProjects([]); setPage(0); setMorePages(true); - }, [accessToken, requestType, iTwinSubClass, data, apiOverrides]); + }, [accessToken, requestType, iTwinSubClass, data, serverEnvironmentPrefix]); React.useEffect(() => { if (!morePages) { @@ -100,7 +101,7 @@ export const useITwinData = ({ ? "" : `&$search=${filterOptions}`; const url = `${_getAPIServer( - apiOverrides + serverEnvironmentPrefix )}/itwins/${endpoint}${subClass}${paging}${search}`; const makeFetchRequest = async () => { @@ -152,7 +153,7 @@ export const useITwinData = ({ accessToken, requestType, data, - apiOverrides, + serverEnvironmentPrefix, filterOptions, page, morePages, diff --git a/packages/modules/imodel-browser/src/containers/ITwinGrid/useITwinFavorites.ts b/packages/modules/imodel-browser/src/containers/ITwinGrid/useITwinFavorites.ts index 41e11ca8..5aab02ce 100644 --- a/packages/modules/imodel-browser/src/containers/ITwinGrid/useITwinFavorites.ts +++ b/packages/modules/imodel-browser/src/containers/ITwinGrid/useITwinFavorites.ts @@ -5,7 +5,6 @@ import { useCallback, useEffect, useState } from "react"; -import { ApiOverrides, ITwinFull } from "../../types"; import { _getAPIServer } from "../../utils/_apiOverrides"; const HOOK_ABORT_ERROR = @@ -24,7 +23,7 @@ const HOOK_ABORT_ERROR = */ export const useITwinFavorites = ( accessToken: string | (() => Promise) | undefined, - apiOverrides?: ApiOverrides + serverEnvironmentPrefix?: "dev" | "qa" | "" ): { iTwinFavorites: Set; addITwinToFavorites: (iTwinId: string) => Promise; @@ -45,7 +44,9 @@ export const useITwinFavorites = ( if (!accessToken || !iTwinId || iTwinId === "") { return; } - const url = `${_getAPIServer(apiOverrides)}/itwins/favorites/${iTwinId}`; + const url = `${_getAPIServer( + serverEnvironmentPrefix + )}/itwins/favorites/${iTwinId}`; try { const result = await fetch(url, { method: "POST", @@ -68,7 +69,7 @@ export const useITwinFavorites = ( console.error(error); } }, - [accessToken, apiOverrides] + [accessToken, serverEnvironmentPrefix] ); /** @@ -81,7 +82,9 @@ export const useITwinFavorites = ( if (!accessToken || !iTwinId || iTwinId === "") { return; } - const url = `${_getAPIServer(apiOverrides)}/itwins/favorites/${iTwinId}`; + const url = `${_getAPIServer( + serverEnvironmentPrefix + )}/itwins/favorites/${iTwinId}`; try { const result = await fetch(url, { method: "DELETE", @@ -108,7 +111,7 @@ export const useITwinFavorites = ( console.error(error); } }, - [accessToken, apiOverrides] + [accessToken, serverEnvironmentPrefix] ); /** @@ -123,7 +126,7 @@ export const useITwinFavorites = ( return []; } const url = `${_getAPIServer( - apiOverrides + serverEnvironmentPrefix )}/itwins/favorites?subClass=Project`; const result = await fetch(url, { headers: { @@ -152,7 +155,7 @@ export const useITwinFavorites = ( const response: ITwinFavoritesResponse = await result.json(); return response.iTwins; }, - [accessToken, apiOverrides, shouldRefetchFavorites] + [accessToken, serverEnvironmentPrefix, shouldRefetchFavorites] ); const resetShouldRefetchFavorites = useCallback(() => { diff --git a/packages/modules/imodel-browser/src/containers/ITwinGrid/useITwinTableConfig.tsx b/packages/modules/imodel-browser/src/containers/ITwinGrid/useITwinTableConfig.tsx index ad5d10e8..2994dbc9 100644 --- a/packages/modules/imodel-browser/src/containers/ITwinGrid/useITwinTableConfig.tsx +++ b/packages/modules/imodel-browser/src/containers/ITwinGrid/useITwinTableConfig.tsx @@ -139,6 +139,8 @@ export const useITwinTableConfig = ({ iTwinActions, iTwinFavorites, removeITwinFromFavorites, + strings.addToFavorites, + strings.removeFromFavorites, strings.tableColumnDescription, strings.tableColumnFavorites, strings.tableColumnLastModified, diff --git a/packages/modules/imodel-browser/src/containers/iModelGrid/IModelGrid.tsx b/packages/modules/imodel-browser/src/containers/iModelGrid/IModelGrid.tsx index a164bffc..c7504935 100644 --- a/packages/modules/imodel-browser/src/containers/iModelGrid/IModelGrid.tsx +++ b/packages/modules/imodel-browser/src/containers/iModelGrid/IModelGrid.tsx @@ -25,7 +25,7 @@ import { useIModelData } from "./useIModelData"; import { useIModelTableConfig } from "./useIModelTableConfig"; export interface IModelGridProps { /** - * Access token that requires the `imodels:read` scope. Provide a function that returns the token to prevent the token from expiring. */ + * Access token that requires the `imodels:read` scope. Must be memoized. Provide a function that returns the token to prevent the token from expiring. */ accessToken?: string | (() => Promise) | undefined; /** ITwin Id to list the iModels from (mutually exclusive to assetId) */ iTwinId?: string | undefined; diff --git a/packages/modules/imodel-browser/src/containers/iModelGrid/useIModelData.ts b/packages/modules/imodel-browser/src/containers/iModelGrid/useIModelData.ts index 426573a4..fdc05b0a 100644 --- a/packages/modules/imodel-browser/src/containers/iModelGrid/useIModelData.ts +++ b/packages/modules/imodel-browser/src/containers/iModelGrid/useIModelData.ts @@ -69,7 +69,7 @@ export const useIModelData = ({ accessToken, iTwinId, apiOverrides?.data, - apiOverrides, + apiOverrides?.serverEnvironmentPrefix, searchText, maxCount, ]); @@ -110,10 +110,9 @@ export const useIModelData = ({ const searching = searchText?.trim() ? `&$search=${searchText}` : ""; const abortController = new AbortController(); - const url = `${_getAPIServer({ - data: apiOverrides?.data, - serverEnvironmentPrefix: apiOverrides?.serverEnvironmentPrefix, - })}/imodels/${selection}${sorting}${paging}${searching}`; + const url = `${_getAPIServer( + apiOverrides?.serverEnvironmentPrefix + )}/imodels/${selection}${sorting}${paging}${searching}`; const makeFetchRequest = async () => { const options: RequestInit = { diff --git a/packages/modules/imodel-browser/src/containers/iModelThumbnail/useIModelThumbnail.ts b/packages/modules/imodel-browser/src/containers/iModelThumbnail/useIModelThumbnail.ts index 30c206cf..f82613e5 100644 --- a/packages/modules/imodel-browser/src/containers/iModelThumbnail/useIModelThumbnail.ts +++ b/packages/modules/imodel-browser/src/containers/iModelThumbnail/useIModelThumbnail.ts @@ -49,7 +49,9 @@ export const useIModelThumbnail = ( }; const response = await fetch( - `${_getAPIServer(apiOverrides)}/imodels/${iModelId}/thumbnail`, + `${_getAPIServer( + apiOverrides?.serverEnvironmentPrefix + )}/imodels/${iModelId}/thumbnail`, options ); const thumbnail: string = response.ok @@ -76,6 +78,12 @@ export const useIModelThumbnail = ( return () => { abortController.abort(); }; - }, [accessToken, iModelId, thumbnail, apiOverrides?.data, apiOverrides]); + }, [ + accessToken, + iModelId, + thumbnail, + apiOverrides?.data, + apiOverrides?.serverEnvironmentPrefix, + ]); return thumbnail; }; diff --git a/packages/modules/imodel-browser/src/utils/_apiOverrides.test.ts b/packages/modules/imodel-browser/src/utils/_apiOverrides.test.ts index 6306b474..3799b1e2 100644 --- a/packages/modules/imodel-browser/src/utils/_apiOverrides.test.ts +++ b/packages/modules/imodel-browser/src/utils/_apiOverrides.test.ts @@ -6,17 +6,17 @@ import { _getAPIServer, _mergeStrings } from "./_apiOverrides"; describe("apiOverrides", () => { describe("getAPIServer", () => { - it("returns https://api.bentley.com with no serverEnvironmentPrefix", () => { - const result = _getAPIServer({}); + it("returns https://api.bentley.com with empty serverEnvironmentPrefix", () => { + const result = _getAPIServer(""); expect(result).toEqual("https://api.bentley.com"); }); it("returns https://[prefix]-api.bentley.com when provided with [prefix]", () => { - const result = _getAPIServer({ serverEnvironmentPrefix: "qa" }); + const result = _getAPIServer("qa"); expect(result).toEqual("https://qa-api.bentley.com"); }); - it("handles undefined overrdies", () => { + it("handles undefined overrides", () => { const result = _getAPIServer(undefined); expect(result).toEqual("https://api.bentley.com"); diff --git a/packages/modules/imodel-browser/src/utils/_apiOverrides.ts b/packages/modules/imodel-browser/src/utils/_apiOverrides.ts index 2a706a18..60902814 100644 --- a/packages/modules/imodel-browser/src/utils/_apiOverrides.ts +++ b/packages/modules/imodel-browser/src/utils/_apiOverrides.ts @@ -2,18 +2,13 @@ * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ -import { ApiOverrides } from "../types"; /** Build APIM server url out of overrides * @private */ -export const _getAPIServer = ( - apiOverrides: ApiOverrides | undefined -) => +export const _getAPIServer = (serverEnvironmentPrefix?: "dev" | "qa" | "") => `https://${ - apiOverrides?.serverEnvironmentPrefix - ? `${apiOverrides.serverEnvironmentPrefix}-` - : "" + serverEnvironmentPrefix ? `${serverEnvironmentPrefix}-` : "" }api.bentley.com`; /**